feat: introduce binding generator
A squash of hundreds of commits with a binding API generator. The API generator works with a hybrid model: 1. We define methods and fields in the code. 2. Mark them with API annotations. 3. For specific use cases, we use the old approach with JNI generation comments. There is also an API to generate an AST tree, which is saved to JSON. This JSON can be used for full API generation by declaring only field/method names. Currently, it is used to generate constants. By eyeballing, 95% of the API has been converted to the new approach. This is sufficient for the current moment. Only edge cases with specific API calls are left. fix: ambiguous call for ImGui::GetColorU32 build: disable checkstyle for imgui-binding module Since the module is heavily generated, checkstyle for it doesn't make sense. build: make buildSrc to use JDK 11 build: make generateApi optional when compiling project Initially generated module was not included to the project VCS. Since it's now in the separate src folder, we don't need to call generator every time. Only when a new generation result is required.
This commit is contained in:
1 parent
e64df5a433
commit
8a8cd22ed6
323 files changed
+113518
-15318
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 &}")
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
package tool.generator.api
|
||||
|
||||
import spoon.reflect.code.CtStatement
|
||||
import spoon.reflect.declaration.CtField
|
||||
import spoon.reflect.declaration.CtMethod
|
||||
import spoon.reflect.declaration.CtParameter
|
||||
import spoon.reflect.declaration.ModifierKind
|
||||
import spoon.reflect.factory.Factory
|
||||
import spoon.reflect.reference.CtTypeReference
|
||||
|
||||
private fun convertParams2jni(f: Factory, params: List<CtParameter<*>>, defaults: IntArray): List<CtParameter<*>> {
|
||||
val result = mutableListOf<CtParameter<*>>()
|
||||
for ((index, p) in params.withIndex()) {
|
||||
if (defaults.isNotEmpty() && !defaults.contains(index)) {
|
||||
continue
|
||||
}
|
||||
if (p.isType("Void")) {
|
||||
continue
|
||||
}
|
||||
if (p.isType("ImVec2") || p.isType("ImVec4")) { // vec param always accepted as primitive values
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}X")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Y")
|
||||
}
|
||||
if (p.isType("ImVec4")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Z")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}W")
|
||||
}
|
||||
}
|
||||
} else if (p.isType("ImRect")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MinX")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MinY")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MaxX")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("float"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MaxY")
|
||||
}
|
||||
} else if (p.isType("ImPlotPoint")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}X")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Y")
|
||||
}
|
||||
} else if (p.isType("ImPlotRange")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Min")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Max")
|
||||
}
|
||||
} else if (p.isType("ImPlotLimits")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MinX")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MinY")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MaxX")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("double"))
|
||||
setSimpleName<Nothing>("${p.simpleName}MaxY")
|
||||
}
|
||||
} else if (p.isType("TextEditorCoordinates")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Line")
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("${p.simpleName}Column")
|
||||
}
|
||||
} else if (p.isType("String[]")) {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("String[]"))
|
||||
setSimpleName<Nothing>(p.simpleName)
|
||||
}
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("${p.simpleName}$PARAM_ARR_LEN_POSTFIX")
|
||||
}
|
||||
} else {
|
||||
result += f.createParameter<Nothing>().apply {
|
||||
val type = if (p.type.isPtrClass()) {
|
||||
f.createTypeParam("long")
|
||||
} else if (p.type.isClass) {
|
||||
when (p.type.simpleName) {
|
||||
"ImBoolean" -> f.createTypeParam("boolean[]")
|
||||
"ImInt" -> f.createTypeParam("int[]")
|
||||
"ImFloat" -> f.createTypeParam("float[]")
|
||||
"ImLong" -> f.createTypeParam("long[]")
|
||||
"ImDouble" -> f.createTypeParam("double[]")
|
||||
else -> p.type
|
||||
}
|
||||
} else {
|
||||
p.type
|
||||
}
|
||||
|
||||
setType<Nothing>(type)
|
||||
setSimpleName<Nothing>(p.simpleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun joinInBodyParams(params: List<CtParameter<*>>, defaults: IntArray): String {
|
||||
fun param2str(p: CtParameter<*>): String {
|
||||
return if (p.type.isPtrClass()) {
|
||||
"reinterpret_cast<${p.type.simpleName}*>(${p.simpleName})"
|
||||
} else if (p.isPrimitivePtrType()) {
|
||||
"&${p.simpleName}[0]"
|
||||
} else if (p.type.isClass) {
|
||||
when (p.type.simpleName) {
|
||||
"ImBoolean", "ImInt", "ImFloat", "ImLong", "ImDouble" -> {
|
||||
"(${p.simpleName} != NULL ? &${p.simpleName}[0] : NULL)"
|
||||
}
|
||||
|
||||
"ImRect" -> {
|
||||
"ImRect(${p.simpleName}MinX, ${p.simpleName}MinY, ${p.simpleName}MaxX, ${p.simpleName}MaxY)"
|
||||
}
|
||||
|
||||
"ImPlotPoint" -> {
|
||||
"ImPlotPoint(${p.simpleName}X, ${p.simpleName}Y)"
|
||||
}
|
||||
|
||||
"ImPlotRange" -> {
|
||||
"ImPlotRange(${p.simpleName}Min, ${p.simpleName}Max)"
|
||||
}
|
||||
|
||||
"ImPlotLimits" -> {
|
||||
"ImPlotLimits(${p.simpleName}MinX, ${p.simpleName}MinY, ${p.simpleName}MaxX, ${p.simpleName}MaxY)"
|
||||
}
|
||||
|
||||
"TextEditorCoordinates" -> {
|
||||
"TextEditor::Coordinates(${p.simpleName}Line, ${p.simpleName}Column)"
|
||||
}
|
||||
|
||||
else -> p.simpleName
|
||||
}
|
||||
} else {
|
||||
p.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
val visibleParams = mutableListOf<String>()
|
||||
for ((index, p) in params.withIndex()) {
|
||||
if (defaults.isNotEmpty() && !defaults.contains(index)) {
|
||||
visibleParams += p.getAnnotation(A_NAME_OPT_ARG)!!.getValueAsString(A_VALUE_CALL_VALUE)
|
||||
continue
|
||||
}
|
||||
if (p.isType("Void")) {
|
||||
visibleParams += p.getAnnotation(A_NAME_ARG_VALUE)?.getValueAsString(A_VALUE_CALL_VALUE) ?: p.simpleName
|
||||
continue
|
||||
}
|
||||
var value = param2str(p)
|
||||
p.getAnnotation(A_NAME_ARG_VALUE)?.let { a ->
|
||||
a.getValueAsString(A_VALUE_CALL_VALUE).takeIf(String::isNotEmpty)?.let { v ->
|
||||
value = v
|
||||
}
|
||||
a.getValueAsString(A_VALUE_CALL_PREFIX).takeIf(String::isNotEmpty)?.let { v ->
|
||||
value = v + value
|
||||
}
|
||||
a.getValueAsString(A_VALUE_CALL_SUFFIX).takeIf(String::isNotEmpty)?.let { v ->
|
||||
value += v
|
||||
}
|
||||
a.getValueAsString(A_VALUE_STATIC_CAST).takeIf(String::isNotEmpty)?.let { v ->
|
||||
value = "static_cast<$v>($value)"
|
||||
}
|
||||
a.getValueAsString(A_VALUE_REINTERPRET_CAST).takeIf(String::isNotEmpty)?.let { v ->
|
||||
value = "reinterpret_cast<$v>($value)"
|
||||
}
|
||||
}
|
||||
visibleParams += value
|
||||
}
|
||||
return visibleParams.joinToString()
|
||||
}
|
||||
|
||||
private fun createMethod(mOrig: CtMethod<*>, params: List<CtParameter<*>>, defaults: IntArray): CtMethod<*> {
|
||||
val f = mOrig.factory
|
||||
val newM = f.createMethod<Nothing>()
|
||||
|
||||
newM.setParent<Nothing>(mOrig.parent)
|
||||
|
||||
newM.addModifier<Nothing>(ModifierKind.PRIVATE)
|
||||
newM.addModifier<Nothing>(ModifierKind.NATIVE)
|
||||
if (mOrig.isStatic) {
|
||||
newM.addModifier<Nothing>(ModifierKind.STATIC)
|
||||
}
|
||||
|
||||
if (DST_RETURN_TYPE_SET.contains(mOrig.type.simpleName)) {
|
||||
newM.setType<Nothing>(f.createTypeParam("void"))
|
||||
} else if (mOrig.type.isPtrClass()) { // classes returned as a pointer
|
||||
newM.setType<Nothing>(f.createTypeParam("long"))
|
||||
} else {
|
||||
newM.setType<Nothing>(mOrig.type)
|
||||
}
|
||||
|
||||
newM.setSimpleName<Nothing>(mOrig.getJniName())
|
||||
|
||||
convertParams2jni(f, params, defaults).forEach {
|
||||
newM.addParameter<Nothing>(it)
|
||||
}
|
||||
|
||||
if (DST_RETURN_TYPE_SET.contains(mOrig.type.simpleName)) {
|
||||
newM.addParameterAt<Nothing>(0, f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(mOrig.type)
|
||||
setSimpleName<Nothing>("dst")
|
||||
})
|
||||
}
|
||||
|
||||
newM.setBody<Nothing>(f.createCodeSnippet(
|
||||
buildString {
|
||||
val jniCpyReturn = DST_RETURN_TYPE_SET.contains(mOrig.type.simpleName)
|
||||
val isStrReturn = mOrig.isType("String")
|
||||
|
||||
if (jniCpyReturn) {
|
||||
append("Jni::${mOrig.type.simpleName}Cpy(env, ")
|
||||
} else {
|
||||
if (!mOrig.isType("void")) {
|
||||
append("return ")
|
||||
if (isStrReturn) {
|
||||
append("env->NewStringUTF(")
|
||||
} else if (!mOrig.type.isPrimitive) {
|
||||
append("($PTR_JNI_CAST)")
|
||||
}
|
||||
mOrig.getAnnotation(A_NAME_RETURN_VALUE)?.let { a ->
|
||||
append(a.getValueAsString(A_VALUE_CALL_PREFIX))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callName = mOrig.getAnnotation(A_NAME_BINDING_METHOD)?.let { a ->
|
||||
a.getValueAsString(A_VALUE_CALL_NAME)?.takeIf(String::isNotEmpty)
|
||||
} ?: mOrig.simpleName
|
||||
|
||||
val callPtr = mOrig.parent.getAnnotation(A_NAME_BINDING_SOURCE)!!.let { a ->
|
||||
a.getValueAsString(A_VALUE_CALL_PTR)?.takeIf(String::isNotEmpty) ?: if (mOrig.isStatic) {
|
||||
mOrig.declaringType.simpleName
|
||||
} else {
|
||||
PTR_JNI_THIS
|
||||
}
|
||||
}
|
||||
|
||||
val callOperator = mOrig.parent.getAnnotation(A_NAME_BINDING_SOURCE)!!.let { a ->
|
||||
a.getValueAsString(A_VALUE_CALL_OPERATOR)?.takeIf(String::isNotEmpty) ?: if (mOrig.isStatic) {
|
||||
"::"
|
||||
} else {
|
||||
"->"
|
||||
}
|
||||
}
|
||||
|
||||
append("$callPtr$callOperator${callName}(")
|
||||
append(joinInBodyParams(params, defaults))
|
||||
append(')')
|
||||
|
||||
mOrig.getAnnotation(A_NAME_RETURN_VALUE)?.let { a ->
|
||||
append(a.getValueAsString(A_VALUE_CALL_SUFFIX))
|
||||
}
|
||||
|
||||
if (jniCpyReturn) {
|
||||
append(", dst)")
|
||||
} else if (isStrReturn) {
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
return newM
|
||||
}
|
||||
|
||||
private fun createMethodVecValueReturn(
|
||||
vecVal: String,
|
||||
mOrig: CtMethod<*>,
|
||||
nameOrig: String,
|
||||
params: List<CtParameter<*>>,
|
||||
defaults: IntArray
|
||||
): CtMethod<*> {
|
||||
val callPtr = mOrig.parent.getAnnotation(A_NAME_BINDING_SOURCE)!!.let { a ->
|
||||
a.getValueAsString(A_VALUE_CALL_PTR)?.takeIf(String::isNotEmpty) ?: if (mOrig.isStatic) {
|
||||
mOrig.declaringType.simpleName
|
||||
} else {
|
||||
PTR_JNI_THIS
|
||||
}
|
||||
}
|
||||
|
||||
val callOperator = mOrig.parent.getAnnotation(A_NAME_BINDING_SOURCE)!!.let { a ->
|
||||
a.getValueAsString(A_VALUE_CALL_OPERATOR)?.takeIf(String::isNotEmpty) ?: if (mOrig.isStatic) {
|
||||
"::"
|
||||
} else {
|
||||
"->"
|
||||
}
|
||||
}
|
||||
|
||||
val mNew = mOrig.clone()
|
||||
mNew.setSimpleName<Nothing>(mOrig.simpleName + vecVal.capitalize())
|
||||
mNew.removeParameter(mNew.parameters[0]) // dst param which was added during orig method creation
|
||||
mNew.type.setSimpleName<Nothing>("float")
|
||||
mNew.setBody<Nothing>(mOrig.factory.createCodeSnippet(
|
||||
buildString {
|
||||
append("return ")
|
||||
append("$callPtr$callOperator$nameOrig(")
|
||||
append(joinInBodyParams(params, defaults))
|
||||
append(").$vecVal")
|
||||
}
|
||||
))
|
||||
return mNew
|
||||
}
|
||||
|
||||
private fun convertManualJni(mOrig: CtMethod<*>, method: CtMethod<*>): CtMethod<*>? {
|
||||
val mNew = method.clone()
|
||||
|
||||
val getLines = mutableListOf<String>()
|
||||
val releaseLines = mutableListOf<String>()
|
||||
|
||||
mNew.parameters.forEach { p ->
|
||||
var isConverted = false
|
||||
|
||||
if (p.isPrimitivePtrType()) {
|
||||
val typeCast = PRIMITIVE_PTR_TYPECAST_MAP[p.type.simpleName]
|
||||
getLines += "auto ${p.simpleName} = obj_${p.simpleName} == NULL ? NULL : (${typeCast})env->GetPrimitiveArrayCritical(obj_${p.simpleName}, JNI_FALSE)"
|
||||
releaseLines += "if (${p.simpleName} != NULL) env->ReleasePrimitiveArrayCritical(obj_${p.simpleName}, ${p.simpleName}, JNI_FALSE)"
|
||||
isConverted = true
|
||||
}
|
||||
|
||||
if (p.isType("String")) {
|
||||
getLines += "auto ${p.simpleName} = obj_${p.simpleName} == NULL ? NULL : (char*)env->GetStringUTFChars(obj_${p.simpleName}, JNI_FALSE)"
|
||||
releaseLines += "if (${p.simpleName} != NULL) env->ReleaseStringUTFChars(obj_${p.simpleName}, ${p.simpleName})"
|
||||
isConverted = true
|
||||
}
|
||||
|
||||
if (p.isType("String[]")) {
|
||||
val countParam = "${p.simpleName}$PARAM_ARR_LEN_POSTFIX"
|
||||
getLines += """
|
||||
const char* ${p.simpleName}[$countParam];
|
||||
for (int i = 0; i < ${p.simpleName}$PARAM_ARR_LEN_POSTFIX; i++) {
|
||||
const jstring str = (jstring)env->GetObjectArrayElement(obj_${p.simpleName}, i);
|
||||
auto rawStr = (char*)env->GetStringUTFChars(str, JNI_FALSE);
|
||||
${p.simpleName}[i] = rawStr;
|
||||
}
|
||||
""".trimIndent()
|
||||
releaseLines += """
|
||||
for (int i = 0; i < $countParam; i++) {
|
||||
const jstring str = (jstring)env->GetObjectArrayElement(obj_${p.simpleName}, i);
|
||||
env->ReleaseStringUTFChars(str, ${p.simpleName}[i]);
|
||||
}
|
||||
""".trimIndent()
|
||||
isConverted = true
|
||||
}
|
||||
|
||||
if (p.isType("ImVec2[]")) {
|
||||
val lengthVar = "${p.simpleName}Length"
|
||||
getLines += """
|
||||
int $lengthVar = env->GetArrayLength(obj_${p.simpleName});
|
||||
ImVec2 ${p.simpleName}[$lengthVar];
|
||||
for (int i = 0; i < $lengthVar; i++) {
|
||||
jobject src = env->GetObjectArrayElement(obj_${p.simpleName}, i);
|
||||
ImVec2 dst;
|
||||
Jni::ImVec2Cpy(env, src, &dst);
|
||||
${p.simpleName}[i] = dst;
|
||||
}
|
||||
""".trimIndent()
|
||||
isConverted = true
|
||||
}
|
||||
|
||||
if (p.isType("ImVec4[]")) {
|
||||
val lengthVar = "${p.simpleName}Length"
|
||||
getLines += """
|
||||
int $lengthVar = env->GetArrayLength(obj_${p.simpleName});
|
||||
ImVec4 ${p.simpleName}[$lengthVar];
|
||||
for (int i = 0; i < $lengthVar; i++) {
|
||||
jobject src = env->GetObjectArrayElement(obj_${p.simpleName}, i);
|
||||
ImVec4 dst;
|
||||
Jni::ImVec4Cpy(env, src, &dst);
|
||||
${p.simpleName}[i] = dst;
|
||||
}
|
||||
""".trimIndent()
|
||||
isConverted = true
|
||||
}
|
||||
|
||||
fun isAddPrefixType(type: CtTypeReference<*>): Boolean {
|
||||
return setOf(
|
||||
"String[]",
|
||||
"ImVec2[]",
|
||||
"ImVec4[]",
|
||||
).contains(type.simpleName)
|
||||
}
|
||||
|
||||
if (isConverted && (!mNew.isType("void") || isAddPrefixType(p.type))) {
|
||||
p.setSimpleName<Nothing>("obj_" + p.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
// Sine we work with an already transformed method, which doesn't contain original information,
|
||||
// we need to process the original method instead. For example, ImVec args are transformed into floats.
|
||||
mOrig.parameters.forEach { p ->
|
||||
// Transformed method may not have the current param. For example, if it's optional.
|
||||
// So for ImVec arguments we check, if there is a parameter with the same name and "X" attached.
|
||||
// It's a sort of marker in our case.
|
||||
if (method.parameters.find { it.simpleName == p.simpleName + "X" } != null) {
|
||||
if (p.isType("ImVec2")) {
|
||||
getLines += "ImVec2 ${p.simpleName} = ImVec2(${p.simpleName}X, ${p.simpleName}Y)"
|
||||
}
|
||||
if (p.isType("ImVec4")) {
|
||||
getLines += "ImVec4 ${p.simpleName} = ImVec4(${p.simpleName}X, ${p.simpleName}Y, ${p.simpleName}Z, ${p.simpleName}W)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getLines.isNotEmpty() || releaseLines.isNotEmpty()) {
|
||||
val bOrigCode = mNew.body.getLastStatement<CtStatement>().prettyprint()
|
||||
|
||||
mNew.body.statements.clear()
|
||||
|
||||
getLines.forEach {
|
||||
mNew.body.addStatement<Nothing>(mNew.factory.createCodeSnippet(it))
|
||||
}
|
||||
|
||||
mNew.body.addStatement<Nothing>(mNew.factory.createCodeSnippet(bOrigCode.replace("return ", "auto _result = ")))
|
||||
|
||||
releaseLines.forEach {
|
||||
mNew.body.addStatement<Nothing>(mNew.factory.createCodeSnippet(it))
|
||||
}
|
||||
|
||||
if (!mNew.isType("void")) {
|
||||
mNew.body.addStatement<Nothing>(mNew.factory.createCodeSnippet("return _result"))
|
||||
}
|
||||
|
||||
return mNew
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun CtMethod<*>.prettyprint(isManual: Boolean): String {
|
||||
return buildString {
|
||||
val str = prettyprint().let {
|
||||
if (isManual) {
|
||||
it.replaceFirst(" {", "; /*MANUAL")
|
||||
} else {
|
||||
it.replaceFirst(" {", "; /*")
|
||||
}
|
||||
}
|
||||
append(str)
|
||||
lastIndexOf("}").let { idx ->
|
||||
replace(idx, idx + 1, "*/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun transformMethodToContent(
|
||||
mOrig: CtMethod<*>,
|
||||
params: List<CtParameter<*>> = emptyList(),
|
||||
defaults: IntArray = intArrayOf()
|
||||
): List<String> {
|
||||
fun CtMethod<*>.printJni(): String {
|
||||
return convertManualJni(mOrig, this)?.prettyprint(true) ?: prettyprint(false)
|
||||
}
|
||||
|
||||
val methods = mutableListOf<String>()
|
||||
val mNew = createMethod(mOrig, params, defaults)
|
||||
|
||||
methods += mNew.printJni()
|
||||
|
||||
if (mOrig.isType("ImVec2") || mOrig.isType("ImVec4")) {
|
||||
methods += createMethodVecValueReturn("x", mNew, mOrig.simpleName, params, defaults).printJni()
|
||||
methods += createMethodVecValueReturn("y", mNew, mOrig.simpleName, params, defaults).printJni()
|
||||
if (mOrig.isType("ImVec4")) {
|
||||
methods += createMethodVecValueReturn("z", mNew, mOrig.simpleName, params, defaults).printJni()
|
||||
methods += createMethodVecValueReturn("w", mNew, mOrig.simpleName, params, defaults).printJni()
|
||||
}
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
fun jniMethodContent(method: CtMethod<*>): List<String> {
|
||||
val methods = mutableListOf<String>()
|
||||
|
||||
for (paramsSize in (findFirstOptParam(method)..method.parameters.size)) {
|
||||
val params = method.parameters.subList(0, paramsSize)
|
||||
methods += transformMethodToContent(method, params)
|
||||
}
|
||||
|
||||
for (defaults in findDefaultsCombinations(method)) {
|
||||
methods += transformMethodToContent(method, method.parameters, defaults)
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
private fun createFieldGetContent(field: CtField<*>): List<String> {
|
||||
val f = field.factory
|
||||
|
||||
val getAccessor = f.createMethod<Nothing>()
|
||||
getAccessor.setParent<Nothing>(field.parent)
|
||||
getAccessor.setSimpleName<Nothing>("get${field.simpleName}")
|
||||
getAccessor.setType<Nothing>(field.type)
|
||||
getAccessor.addModifier<Nothing>(ModifierKind.PRIVATE)
|
||||
getAccessor.setAnnotations<Nothing>(field.annotations)
|
||||
|
||||
val result = mutableListOf<String>()
|
||||
|
||||
if (field.hasAnnotation(A_NAME_TYPE_ARRAY)) {
|
||||
val arrayType = field.getAnnotation(A_NAME_TYPE_ARRAY)!!.getValueAsString(A_VALUE_TYPE)
|
||||
val arraySize = field.getAnnotation(A_NAME_TYPE_ARRAY)!!.getValueAsString(A_VALUE_SIZE)
|
||||
|
||||
val getArray = getAccessor.clone()
|
||||
getArray.addModifier<Nothing>(ModifierKind.NATIVE)
|
||||
getArray.setSimpleName<Nothing>("nGet${field.simpleName}")
|
||||
getArray.setType<Nothing>(f.createTypeParam("$arrayType[]"))
|
||||
|
||||
when (arrayType) {
|
||||
"boolean", "short", "int", "float", "double", "long" -> {
|
||||
getArray.setBody<Nothing>(
|
||||
f.createCodeSnippet(
|
||||
"""
|
||||
j$arrayType jBuf[$arraySize];
|
||||
for (int i = 0; i < $arraySize; i++)
|
||||
jBuf[i] = $PTR_JNI_THIS->${field.getCallName()}[i];
|
||||
j${arrayType}Array result = env->New${arrayType.capitalize()}Array($arraySize);
|
||||
env->Set${arrayType.capitalize()}ArrayRegion(result, 0, $arraySize, jBuf);
|
||||
return result
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"ImVec2", "ImVec4" -> {
|
||||
getArray.setBody<Nothing>(
|
||||
f.createCodeSnippet(
|
||||
"""
|
||||
return Jni::New${arrayType}Array(env, $PTR_JNI_THIS->${field.getCallName()}, $arraySize)
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
result += getArray.prettyprint(false)
|
||||
|
||||
when (arrayType) {
|
||||
"boolean", "short", "int", "float", "double", "long" -> {
|
||||
val getArrayIdx = getArray.clone()
|
||||
getArrayIdx.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("idx")
|
||||
})
|
||||
getArrayIdx.setType<Nothing>(f.createTypeParam(arrayType))
|
||||
getArrayIdx.setBody<Nothing>(f.createCodeSnippet("return $PTR_JNI_THIS->${field.getCallName()}[idx]"))
|
||||
|
||||
result += getArrayIdx.prettyprint(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getAccessor.addAnnotation<Nothing>(f.createAnnotation(f.createTypeReference<Nothing?>().apply {
|
||||
setSimpleName<Nothing>("imgui.binding.annotation.BindingMethod")
|
||||
}).apply {
|
||||
addValue<Nothing>(A_VALUE_CALL_NAME, field.getCallName())
|
||||
})
|
||||
|
||||
result += transformMethodToContent(getAccessor).map {
|
||||
it.replace("$PTR_JNI_THIS->${getAccessor.simpleName}()", "$PTR_JNI_THIS->${field.getCallName()}")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun createFieldSetContent(field: CtField<*>): List<String> {
|
||||
val f = field.factory
|
||||
|
||||
val setAccessor = f.createMethod<Nothing>()
|
||||
setAccessor.setType<Nothing>(f.createTypeParam("void"))
|
||||
setAccessor.setParent<Nothing>(field.parent)
|
||||
setAccessor.setSimpleName<Nothing>("set${field.simpleName}")
|
||||
setAccessor.addModifier<Nothing>(ModifierKind.PRIVATE)
|
||||
|
||||
val valueParam = f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(field.type)
|
||||
setSimpleName<Nothing>("value")
|
||||
}
|
||||
|
||||
// Add this param to the method placeholder, since some transformations can check it.
|
||||
setAccessor.addParameter<Nothing>(valueParam)
|
||||
|
||||
val result = mutableListOf<String>()
|
||||
|
||||
if (field.hasAnnotation(A_NAME_TYPE_ARRAY)) {
|
||||
val arrayType = field.getAnnotation(A_NAME_TYPE_ARRAY)!!.getValueAsString(A_VALUE_TYPE)
|
||||
val arraySize = field.getAnnotation(A_NAME_TYPE_ARRAY)!!.getValueAsString(A_VALUE_SIZE)
|
||||
|
||||
val setArray = setAccessor.clone()
|
||||
setArray.addModifier<Nothing>(ModifierKind.NATIVE)
|
||||
setArray.setSimpleName<Nothing>("nSet${field.simpleName}")
|
||||
|
||||
when (arrayType) {
|
||||
"boolean", "short", "int", "float", "double", "long" -> {
|
||||
setArray.setBody<Nothing>(
|
||||
f.createCodeSnippet(
|
||||
"""
|
||||
for (int i = 0; i < $arraySize; i++)
|
||||
$PTR_JNI_THIS->${field.getCallName()}[i] = value[i]
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"ImVec2", "ImVec4" -> {
|
||||
setArray.setBody<Nothing>(
|
||||
f.createCodeSnippet("""
|
||||
Jni::${arrayType}ArrayCpy(env, value, $PTR_JNI_THIS->${field.getCallName()}, $arraySize)
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
result += setArray.prettyprint(false)
|
||||
|
||||
when (arrayType) {
|
||||
"boolean", "short", "int", "float", "double", "long" -> {
|
||||
val setArrayIdx = setArray.clone()
|
||||
setArrayIdx.parameters.clear()
|
||||
setArrayIdx.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("idx")
|
||||
})
|
||||
setArrayIdx.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam(arrayType))
|
||||
setSimpleName<Nothing>("value")
|
||||
})
|
||||
setArrayIdx.setType<Nothing>(f.createTypeParam(arrayType))
|
||||
setArrayIdx.setBody<Nothing>(f.createCodeSnippet("$PTR_JNI_THIS->${field.getCallName()}[idx] = value"))
|
||||
|
||||
result += setArrayIdx.prettyprint(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result += transformMethodToContent(setAccessor, listOf(valueParam)).map {
|
||||
it.replace(
|
||||
"$PTR_JNI_THIS->${setAccessor.simpleName}\\((.+)\\)".toRegex(),
|
||||
"$PTR_JNI_THIS->${field.getCallName()} = $1"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun jniFieldContent(field: CtField<*>): List<String> {
|
||||
val fields = mutableListOf<String>()
|
||||
val a = field.getAnnotation(A_NAME_BINDING_FIELD)
|
||||
if (a?.containsValue(A_VALUE_ACCESSORS, A_TYPE_VALUE_ACCESSOR_GETTER) == true) {
|
||||
fields += createFieldGetContent(field)
|
||||
}
|
||||
if (a?.containsValue(A_VALUE_ACCESSORS, A_TYPE_VALUE_ACCESSOR_SETTER) == true) {
|
||||
fields += createFieldSetContent(field)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
package tool.generator.api
|
||||
|
||||
import spoon.reflect.code.CtJavaDoc
|
||||
import spoon.reflect.code.CtJavaDocTag
|
||||
import spoon.reflect.declaration.CtField
|
||||
import spoon.reflect.declaration.CtMethod
|
||||
import spoon.reflect.declaration.CtParameter
|
||||
import spoon.reflect.declaration.ModifierKind
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
private fun createStaticStructFieldName(method: CtMethod<*>): String {
|
||||
return "_${method.simpleName.toUpperCase()}_${method.parameters.hashCode().absoluteValue}"
|
||||
}
|
||||
|
||||
private fun createStaticStructField(typeName: String, fieldName: String): String {
|
||||
return "private static final $typeName $fieldName = new $typeName(0);"
|
||||
}
|
||||
|
||||
private fun joinInBodyParams(params: List<CtParameter<*>>, defaults: IntArray): String {
|
||||
fun param2str(p: CtParameter<*>): String {
|
||||
return if (p.type.isPtrClass()) {
|
||||
"${p.simpleName}.$PTR_JVM_FIELD"
|
||||
} else if (p.type.isClass) {
|
||||
when (p.type.simpleName) {
|
||||
"ImBoolean", "ImShort", "ImInt", "ImFloat", "ImLong", "ImDouble" -> {
|
||||
"${p.simpleName} != null ? ${p.simpleName}.getData() : null"
|
||||
}
|
||||
|
||||
"ImVec2" -> "${p.simpleName}.x, ${p.simpleName}.y"
|
||||
"ImVec4" -> "${p.simpleName}.x, ${p.simpleName}.y, ${p.simpleName}.z, ${p.simpleName}.w"
|
||||
"ImRect" -> "${p.simpleName}.min.x, ${p.simpleName}.min.y, ${p.simpleName}.max.x, ${p.simpleName}.max.y"
|
||||
|
||||
"ImPlotPoint" -> "${p.simpleName}.x, ${p.simpleName}.y"
|
||||
"ImPlotRange" -> "${p.simpleName}.min, ${p.simpleName}.max"
|
||||
"ImPlotLimits" -> "${p.simpleName}.x.min, ${p.simpleName}.y.min, ${p.simpleName}.x.max, ${p.simpleName}.y.max"
|
||||
|
||||
"TextEditorCoordinates" -> "${p.simpleName}.mLine, ${p.simpleName}.mColumn"
|
||||
|
||||
"String[]" -> "${p.simpleName}, ${p.simpleName}.length"
|
||||
|
||||
"boolean[]", "byte[]", "short[]", "int[]", "float[]", "long[]", "double[]" -> p.simpleName
|
||||
|
||||
else -> p.simpleName
|
||||
}
|
||||
} else {
|
||||
p.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
val visibleParams = mutableListOf<String>()
|
||||
for ((index, p) in params.withIndex()) {
|
||||
if (defaults.isNotEmpty() && !defaults.contains(index)) {
|
||||
continue
|
||||
}
|
||||
if (p.isType("Void")) {
|
||||
continue
|
||||
}
|
||||
visibleParams += param2str(p)
|
||||
}
|
||||
return visibleParams.joinToString()
|
||||
}
|
||||
|
||||
private fun createBodyStaticStructReturn(
|
||||
method: CtMethod<*>,
|
||||
params: List<CtParameter<*>>,
|
||||
defaults: IntArray
|
||||
): String {
|
||||
val fieldName = createStaticStructFieldName(method)
|
||||
return buildString {
|
||||
append("${fieldName}.$PTR_JVM_FIELD = ${method.getJniName()}(")
|
||||
append(joinInBodyParams(params, defaults))
|
||||
appendLine(");")
|
||||
append("return $fieldName")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBodyStructReturn(
|
||||
method: CtMethod<*>,
|
||||
params: List<CtParameter<*>>,
|
||||
defaults: IntArray
|
||||
): String {
|
||||
return buildString {
|
||||
append("return new ${method.type.simpleName}(${method.getJniName()}(")
|
||||
append(joinInBodyParams(params, defaults))
|
||||
append("))")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBodyDstReturn(
|
||||
method: CtMethod<*>,
|
||||
params: List<CtParameter<*>>,
|
||||
defaults: IntArray
|
||||
): String {
|
||||
val type = method.type.simpleName
|
||||
return buildString {
|
||||
appendLine("final $type dst = new ${type}();")
|
||||
append("${method.getJniName()}(dst")
|
||||
joinInBodyParams(params, defaults).let {
|
||||
if (it.isNotEmpty()) {
|
||||
append(", $it")
|
||||
}
|
||||
}
|
||||
appendLine(");")
|
||||
append("return dst")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMethod(origM: CtMethod<*>, params: List<CtParameter<*>>, defaults: IntArray): CtMethod<*> {
|
||||
val f = origM.factory
|
||||
val newM = f.createMethod<Nothing>()
|
||||
|
||||
if (origM.isPublic) {
|
||||
newM.addModifier<Nothing>(ModifierKind.PUBLIC)
|
||||
}
|
||||
if (origM.isStatic) {
|
||||
newM.addModifier<Nothing>(ModifierKind.STATIC)
|
||||
}
|
||||
|
||||
if (origM.docComment.isNotBlank()) {
|
||||
newM.setDocComment<Nothing>(origM.docComment)
|
||||
}
|
||||
|
||||
newM.setAnnotations<Nothing>(origM.annotations)
|
||||
newM.setType<Nothing>(origM.type)
|
||||
newM.setSimpleName<Nothing>(origM.getName())
|
||||
|
||||
for ((index, p) in params.withIndex()) {
|
||||
if (defaults.isNotEmpty() && !defaults.contains(index)) {
|
||||
continue
|
||||
}
|
||||
if (p.isType("Void")) {
|
||||
continue
|
||||
}
|
||||
newM.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
addModifier<Nothing>(ModifierKind.FINAL)
|
||||
setType<Nothing>(p.type)
|
||||
setSimpleName<Nothing>(p.simpleName)
|
||||
})
|
||||
}
|
||||
|
||||
sanitizeDocComment(newM)
|
||||
sanitizeAnnotations(newM)
|
||||
|
||||
newM.setBody<Nothing>(f.createCodeSnippet(
|
||||
buildString {
|
||||
if (origM.isStaticStructReturnValue()) {
|
||||
append(createBodyStaticStructReturn(origM, params, defaults))
|
||||
} else if (DST_RETURN_TYPE_SET.contains(origM.type.simpleName)) {
|
||||
append(createBodyDstReturn(origM, params, defaults))
|
||||
} else if (origM.type.isClass && !origM.type.isPrimitive && !origM.isType("void") && !origM.isType("String") && !origM.isPrimitivePtrType()) {
|
||||
append(createBodyStructReturn(origM, params, defaults))
|
||||
} else {
|
||||
if (!origM.isType("void")) {
|
||||
append("return ")
|
||||
}
|
||||
append("${origM.getJniName()}(")
|
||||
append(joinInBodyParams(params, defaults))
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
return newM
|
||||
}
|
||||
|
||||
private fun methodVecUnwrappedContent(method: CtMethod<*>): String {
|
||||
val newMethod = method.clone()
|
||||
val newParams = mutableListOf<CtParameter<*>>()
|
||||
val vecParamNames = mutableSetOf<String>()
|
||||
|
||||
for (p in newMethod.parameters) {
|
||||
if (p.isType("ImVec2") || p.isType("ImVec4")) {
|
||||
vecParamNames += p.simpleName
|
||||
|
||||
val paramX = p.factory.createParameter<Nothing>()
|
||||
paramX.addModifier<Nothing>(ModifierKind.FINAL)
|
||||
paramX.setType<Nothing>(p.factory.createTypeParam("float"))
|
||||
paramX.setSimpleName<Nothing>("${p.simpleName}X")
|
||||
|
||||
val paramY = paramX.clone()
|
||||
paramY.setSimpleName<Nothing>("${p.simpleName}Y")
|
||||
|
||||
newParams += paramX
|
||||
newParams += paramY
|
||||
|
||||
if (p.isType("ImVec4")) {
|
||||
val paramZ = paramX.clone()
|
||||
paramZ.setSimpleName<Nothing>("${p.simpleName}Z")
|
||||
val paramW = paramX.clone()
|
||||
paramW.setSimpleName<Nothing>("${p.simpleName}W")
|
||||
|
||||
newParams += paramZ
|
||||
newParams += paramW
|
||||
}
|
||||
|
||||
getJDoc(newMethod)?.let { jDoc ->
|
||||
val idx = jDoc.tags.indexOfFirst { it.param == p.simpleName }
|
||||
if (idx != -1) {
|
||||
jDoc.removeTag<Nothing>(idx)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newParams += p
|
||||
}
|
||||
}
|
||||
|
||||
newMethod.setParameters<Nothing>(newParams)
|
||||
|
||||
val mContentOrig = method.prettyprint()
|
||||
val mContent = newMethod.prettyprint().let {
|
||||
var str = it
|
||||
for (paramName in vecParamNames) {
|
||||
str = str.replace("${paramName}.x, ${paramName}.y", "${paramName}X, ${paramName}Y")
|
||||
str = str.replace("${paramName}.z, ${paramName}.w", "${paramName}Z, ${paramName}W")
|
||||
}
|
||||
str
|
||||
}
|
||||
|
||||
if (mContent != mContentOrig) {
|
||||
return mContent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun methodRectUnwrappedContent(method: CtMethod<*>): String {
|
||||
val newMethod = method.clone()
|
||||
val newParams = mutableListOf<CtParameter<*>>()
|
||||
val paramNames = mutableSetOf<String>()
|
||||
|
||||
for (p in newMethod.parameters) {
|
||||
if (p.isType("ImRect")) {
|
||||
paramNames += p.simpleName
|
||||
|
||||
val paramMinX = p.factory.createParameter<Nothing>()
|
||||
paramMinX.addModifier<Nothing>(ModifierKind.FINAL)
|
||||
paramMinX.setType<Nothing>(p.factory.createTypeParam("float"))
|
||||
paramMinX.setSimpleName<Nothing>("${p.simpleName}MinX")
|
||||
|
||||
val paramMinY = paramMinX.clone()
|
||||
paramMinY.setSimpleName<Nothing>("${p.simpleName}MinY")
|
||||
|
||||
newParams += paramMinX
|
||||
newParams += paramMinY
|
||||
|
||||
val paramMaxX = paramMinX.clone()
|
||||
paramMaxX.setSimpleName<Nothing>("${p.simpleName}MaxX")
|
||||
val paramMaxY = paramMinX.clone()
|
||||
paramMaxY.setSimpleName<Nothing>("${p.simpleName}MaxY")
|
||||
|
||||
newParams += paramMaxX
|
||||
newParams += paramMaxY
|
||||
} else {
|
||||
newParams += p
|
||||
}
|
||||
}
|
||||
|
||||
newMethod.setParameters<Nothing>(newParams)
|
||||
|
||||
val mContentOrig = method.prettyprint()
|
||||
val mContent = newMethod.prettyprint().let {
|
||||
var str = it
|
||||
for (paramName in paramNames) {
|
||||
str = str.replace(
|
||||
"${paramName}.min.x, ${paramName}.min.y, ${paramName}.max.x, ${paramName}.max.y",
|
||||
"${paramName}MinX, ${paramName}MinY, ${paramName}MaxX, ${paramName}MaxY"
|
||||
)
|
||||
}
|
||||
str
|
||||
}
|
||||
|
||||
if (mContent != mContentOrig) {
|
||||
return mContent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun methodPlotPointUnwrappedContent(method: CtMethod<*>): String {
|
||||
val newMethod = method.clone()
|
||||
val newParams = mutableListOf<CtParameter<*>>()
|
||||
val paramNames = mutableSetOf<String>()
|
||||
|
||||
for (p in newMethod.parameters) {
|
||||
if (p.isType("ImPlotPoint")) {
|
||||
paramNames += p.simpleName
|
||||
|
||||
val paramX = p.factory.createParameter<Nothing>()
|
||||
paramX.addModifier<Nothing>(ModifierKind.FINAL)
|
||||
paramX.setType<Nothing>(p.factory.createTypeParam("double"))
|
||||
paramX.setSimpleName<Nothing>("${p.simpleName}X")
|
||||
|
||||
val paramY = paramX.clone()
|
||||
paramY.setSimpleName<Nothing>("${p.simpleName}Y")
|
||||
|
||||
newParams += paramX
|
||||
newParams += paramY
|
||||
} else {
|
||||
newParams += p
|
||||
}
|
||||
}
|
||||
|
||||
newMethod.setParameters<Nothing>(newParams)
|
||||
|
||||
val mContentOrig = method.prettyprint()
|
||||
val mContent = newMethod.prettyprint().let {
|
||||
var str = it
|
||||
for (paramName in paramNames) {
|
||||
str = str.replace("${paramName}.x, ${paramName}.y", "${paramName}X, ${paramName}Y")
|
||||
}
|
||||
str
|
||||
}
|
||||
|
||||
if (mContent != mContentOrig) {
|
||||
return mContent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun methodPlotRangeUnwrappedContent(method: CtMethod<*>): String {
|
||||
val newMethod = method.clone()
|
||||
val newParams = mutableListOf<CtParameter<*>>()
|
||||
val paramNames = mutableSetOf<String>()
|
||||
|
||||
for (p in newMethod.parameters) {
|
||||
if (p.isType("ImPlotRange")) {
|
||||
paramNames += p.simpleName
|
||||
|
||||
val paramX = p.factory.createParameter<Nothing>()
|
||||
paramX.addModifier<Nothing>(ModifierKind.FINAL)
|
||||
paramX.setType<Nothing>(p.factory.createTypeParam("double"))
|
||||
paramX.setSimpleName<Nothing>("${p.simpleName}Min")
|
||||
|
||||
val paramY = paramX.clone()
|
||||
paramY.setSimpleName<Nothing>("${p.simpleName}Max")
|
||||
|
||||
newParams += paramX
|
||||
newParams += paramY
|
||||
} else {
|
||||
newParams += p
|
||||
}
|
||||
}
|
||||
|
||||
newMethod.setParameters<Nothing>(newParams)
|
||||
|
||||
val mContentOrig = method.prettyprint()
|
||||
val mContent = newMethod.prettyprint().let {
|
||||
var str = it
|
||||
for (paramName in paramNames) {
|
||||
str = str.replace("${paramName}.min, ${paramName}.max", "${paramName}Min, ${paramName}Max")
|
||||
}
|
||||
str
|
||||
}
|
||||
|
||||
if (mContent != mContentOrig) {
|
||||
return mContent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun methodPlotLimitsUnwrappedContent(method: CtMethod<*>): String {
|
||||
val newMethod = method.clone()
|
||||
val newParams = mutableListOf<CtParameter<*>>()
|
||||
val paramNames = mutableSetOf<String>()
|
||||
|
||||
for (p in newMethod.parameters) {
|
||||
if (p.isType("ImPlotLimits")) {
|
||||
paramNames += p.simpleName
|
||||
|
||||
val paramMinX = p.factory.createParameter<Nothing>()
|
||||
paramMinX.addModifier<Nothing>(ModifierKind.FINAL)
|
||||
paramMinX.setType<Nothing>(p.factory.createTypeParam("double"))
|
||||
paramMinX.setSimpleName<Nothing>("${p.simpleName}MinX")
|
||||
|
||||
val paramMinY = paramMinX.clone()
|
||||
paramMinY.setSimpleName<Nothing>("${p.simpleName}MinY")
|
||||
|
||||
newParams += paramMinX
|
||||
newParams += paramMinY
|
||||
|
||||
val paramMaxX = paramMinX.clone()
|
||||
paramMaxX.setSimpleName<Nothing>("${p.simpleName}MaxX")
|
||||
val paramMaxY = paramMinX.clone()
|
||||
paramMaxY.setSimpleName<Nothing>("${p.simpleName}MaxY")
|
||||
|
||||
newParams += paramMaxX
|
||||
newParams += paramMaxY
|
||||
} else {
|
||||
newParams += p
|
||||
}
|
||||
}
|
||||
|
||||
newMethod.setParameters<Nothing>(newParams)
|
||||
|
||||
val mContentOrig = method.prettyprint()
|
||||
val mContent = newMethod.prettyprint().let {
|
||||
var str = it
|
||||
for (paramName in paramNames) {
|
||||
str = str.replace(
|
||||
"${paramName}.x.min, ${paramName}.y.min, ${paramName}.x.max, ${paramName}.y.max",
|
||||
"${paramName}MinX, ${paramName}MinY, ${paramName}MaxX, ${paramName}MaxY"
|
||||
)
|
||||
}
|
||||
str
|
||||
}
|
||||
|
||||
if (mContent != mContentOrig) {
|
||||
return mContent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun methodCoordinatesUnwrappedContent(method: CtMethod<*>): String {
|
||||
val newMethod = method.clone()
|
||||
val newParams = mutableListOf<CtParameter<*>>()
|
||||
val paramNames = mutableSetOf<String>()
|
||||
|
||||
for (p in newMethod.parameters) {
|
||||
if (p.isType("TextEditorCoordinates")) {
|
||||
paramNames += p.simpleName
|
||||
|
||||
val paramX = p.factory.createParameter<Nothing>()
|
||||
paramX.addModifier<Nothing>(ModifierKind.FINAL)
|
||||
paramX.setType<Nothing>(p.factory.createTypeParam("int"))
|
||||
paramX.setSimpleName<Nothing>("${p.simpleName}Line")
|
||||
|
||||
val paramY = paramX.clone()
|
||||
paramY.setSimpleName<Nothing>("${p.simpleName}Column")
|
||||
|
||||
newParams += paramX
|
||||
newParams += paramY
|
||||
} else {
|
||||
newParams += p
|
||||
}
|
||||
}
|
||||
|
||||
newMethod.setParameters<Nothing>(newParams)
|
||||
|
||||
val mContentOrig = method.prettyprint()
|
||||
val mContent = newMethod.prettyprint().let {
|
||||
var str = it
|
||||
for (paramName in paramNames) {
|
||||
str = str.replace("${paramName}.mLine, ${paramName}.mColumn", "${paramName}Line, ${paramName}Column")
|
||||
}
|
||||
str
|
||||
}
|
||||
|
||||
if (mContent != mContentOrig) {
|
||||
return mContent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun createMethodDstReturn(
|
||||
mOrig: CtMethod<*>,
|
||||
params: List<CtParameter<*>>,
|
||||
defaults: IntArray
|
||||
): CtMethod<*> {
|
||||
val mNew = mOrig.clone()
|
||||
mNew.setType<Nothing>(mNew.factory.createTypeParam("void"))
|
||||
mNew.addParameterAt<Nothing>(0, mOrig.factory.createParameter<Nothing>().apply {
|
||||
addModifier<Nothing>(ModifierKind.FINAL)
|
||||
setType<Nothing>(factory.createTypeParam(mOrig.type.simpleName))
|
||||
setSimpleName<Nothing>("dst")
|
||||
})
|
||||
mNew.setBody<Nothing>(mOrig.factory.createCodeSnippet(
|
||||
buildString {
|
||||
append("${mOrig.getJniName()}(dst")
|
||||
joinInBodyParams(params, defaults).let {
|
||||
if (it.isNotEmpty()) {
|
||||
append(", $it")
|
||||
}
|
||||
}
|
||||
append(")")
|
||||
}
|
||||
))
|
||||
return mNew
|
||||
}
|
||||
|
||||
private fun createMethodVecValueReturn(
|
||||
vecVal: String,
|
||||
mOrig: CtMethod<*>,
|
||||
params: List<CtParameter<*>>,
|
||||
defaults: IntArray
|
||||
): CtMethod<*> {
|
||||
val mNew = mOrig.clone()
|
||||
mNew.setSimpleName<Nothing>(mOrig.getName() + vecVal.capitalize())
|
||||
mNew.type.setSimpleName<Nothing>("float")
|
||||
mNew.setBody<Nothing>(mOrig.factory.createCodeSnippetStatement().apply {
|
||||
setValue<Nothing>(buildString {
|
||||
append("return ")
|
||||
append("${mNew.getJniName()}(")
|
||||
append(joinInBodyParams(params, defaults))
|
||||
append(")")
|
||||
})
|
||||
})
|
||||
return mNew
|
||||
}
|
||||
|
||||
private fun transformMethodToContent(
|
||||
mOrig: CtMethod<*>,
|
||||
params: List<CtParameter<*>> = emptyList(),
|
||||
defaults: IntArray = intArrayOf()
|
||||
): List<String> {
|
||||
val methods = mutableListOf<String>()
|
||||
val mNew = createMethod(mOrig, params, defaults)
|
||||
|
||||
methods += mNew.prettyprint()
|
||||
|
||||
if (params.find { it.isType("ImVec2") || it.isType("ImVec4") } != null) {
|
||||
methodVecUnwrappedContent(mNew).takeIf(String::isNotEmpty)?.run(methods::add)
|
||||
}
|
||||
|
||||
if (params.find { it.isType("ImRect") } != null) {
|
||||
methodRectUnwrappedContent(mNew).takeIf(String::isNotEmpty)?.run(methods::add)
|
||||
}
|
||||
|
||||
if (params.find { it.isType("ImPlotPoint") } != null) {
|
||||
methodPlotPointUnwrappedContent(mNew).takeIf(String::isNotEmpty)?.run(methods::add)
|
||||
}
|
||||
|
||||
if (params.find { it.isType("ImPlotRange") } != null) {
|
||||
methodPlotRangeUnwrappedContent(mNew).takeIf(String::isNotEmpty)?.run(methods::add)
|
||||
}
|
||||
|
||||
if (params.find { it.isType("ImPlotLimits") } != null) {
|
||||
methodPlotLimitsUnwrappedContent(mNew).takeIf(String::isNotEmpty)?.run(methods::add)
|
||||
}
|
||||
|
||||
if (params.find { it.isType("TextEditorCoordinates") } != null) {
|
||||
methodCoordinatesUnwrappedContent(mNew).takeIf(String::isNotEmpty)?.run(methods::add)
|
||||
}
|
||||
|
||||
if (mOrig.isType("ImVec2") || mOrig.isType("ImVec4")) {
|
||||
methods += createMethodVecValueReturn("x", mNew, params, defaults).prettyprint()
|
||||
methods += createMethodVecValueReturn("y", mNew, params, defaults).prettyprint()
|
||||
if (mOrig.isType("ImVec4")) {
|
||||
methods += createMethodVecValueReturn("z", mNew, params, defaults).prettyprint()
|
||||
methods += createMethodVecValueReturn("w", mNew, params, defaults).prettyprint()
|
||||
}
|
||||
}
|
||||
|
||||
if (DST_RETURN_TYPE_SET.contains(mOrig.type.simpleName)) {
|
||||
methods += createMethodDstReturn(mNew, params, defaults).prettyprint()
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
fun jvmMethodContent(method: CtMethod<*>): List<String> {
|
||||
val addContent = mutableListOf<String>()
|
||||
|
||||
if (method.isStaticStructReturnValue()) {
|
||||
val staticStructFieldName = createStaticStructFieldName(method)
|
||||
addContent += createStaticStructField(method.type.simpleName, staticStructFieldName)
|
||||
}
|
||||
|
||||
val methods = mutableListOf<String>()
|
||||
|
||||
for (paramsSize in (findFirstOptParam(method)..method.parameters.size)) {
|
||||
val params = method.parameters.subList(0, paramsSize)
|
||||
methods += transformMethodToContent(method, params)
|
||||
}
|
||||
|
||||
for (defaults in findDefaultsCombinations(method)) {
|
||||
methods += transformMethodToContent(method, method.parameters, defaults)
|
||||
}
|
||||
|
||||
return addContent + methods
|
||||
}
|
||||
|
||||
private fun CtMethod<*>.isStaticStructReturnValue(): Boolean {
|
||||
return !isType("void") && getAnnotation(A_NAME_RETURN_VALUE)?.getValueAsObject(A_VALUE_IS_STATIC) == true
|
||||
}
|
||||
|
||||
private fun createFieldGetContent(field: CtField<*>): List<String> {
|
||||
val f = field.factory
|
||||
|
||||
val getAccessor = f.createMethod<Nothing>()
|
||||
getAccessor.setParent<Nothing>(field.parent)
|
||||
getAccessor.setSimpleName<Nothing>("get${field.simpleName}")
|
||||
getAccessor.setType<Nothing>(field.type)
|
||||
getAccessor.addModifier<Nothing>(ModifierKind.PUBLIC)
|
||||
getAccessor.setDocComment<Nothing>(field.docComment)
|
||||
getAccessor.setAnnotations<Nothing>(field.annotations)
|
||||
|
||||
val result = jvmMethodContent(getAccessor).toMutableList()
|
||||
|
||||
if (field.hasAnnotation(A_NAME_TYPE_ARRAY)) {
|
||||
when (val arrayType = field.getAnnotation(A_NAME_TYPE_ARRAY)!!.getValueAsString(A_VALUE_TYPE)) {
|
||||
"boolean", "short", "int", "float", "double", "long" -> {
|
||||
val newM = getAccessor.clone()
|
||||
newM.setType<Nothing>(f.createTypeParam(arrayType))
|
||||
newM.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("idx")
|
||||
})
|
||||
result += jvmMethodContent(newM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun createFieldSetContent(field: CtField<*>): List<String> {
|
||||
val f = field.factory
|
||||
|
||||
val setAccessor = f.createMethod<Nothing>()
|
||||
setAccessor.setType<Nothing>(f.createTypeParam("void"))
|
||||
setAccessor.setParent<Nothing>(field.parent)
|
||||
setAccessor.setSimpleName<Nothing>("set${field.simpleName}")
|
||||
setAccessor.addModifier<Nothing>(ModifierKind.PUBLIC)
|
||||
setAccessor.setDocComment<Nothing>(field.docComment)
|
||||
setAccessor.setAnnotations<Nothing>(field.annotations)
|
||||
|
||||
val valueParam = f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(field.type)
|
||||
setSimpleName<Nothing>("value")
|
||||
}
|
||||
|
||||
val result = transformMethodToContent(setAccessor, listOf(valueParam)).toMutableList()
|
||||
|
||||
if (field.hasAnnotation(A_NAME_TYPE_ARRAY)) {
|
||||
when (val arrayType = field.getAnnotation(A_NAME_TYPE_ARRAY)!!.getValueAsString(A_VALUE_TYPE)) {
|
||||
"boolean", "short", "int", "float", "double", "long" -> {
|
||||
val newM = setAccessor.clone()
|
||||
newM.parameters.clear()
|
||||
newM.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("idx")
|
||||
})
|
||||
newM.addParameter<Nothing>(f.createParameter<Nothing>().apply {
|
||||
setType<Nothing>(f.createTypeParam(arrayType))
|
||||
setSimpleName<Nothing>("value")
|
||||
})
|
||||
result += transformMethodToContent(newM, newM.parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun createFieldFlagUtils(field: CtField<*>): List<String> {
|
||||
val mAdd = field.factory.createMethod<Nothing>()
|
||||
mAdd.setSimpleName<Nothing>("add${field.simpleName}")
|
||||
mAdd.setType<Nothing>(field.factory.createTypeParam("void"))
|
||||
mAdd.addModifier<Nothing>(ModifierKind.PUBLIC)
|
||||
mAdd.setDocComment<Nothing>(field.docComment)
|
||||
mAdd.addParameter<Nothing>(field.factory.createParameter<Nothing>().apply {
|
||||
addModifier<Nothing>(ModifierKind.FINAL)
|
||||
setType<Nothing>(field.factory.createTypeParam("int"))
|
||||
setSimpleName<Nothing>("flags")
|
||||
})
|
||||
mAdd.setBody<Nothing>(field.factory.createCodeSnippet("set${field.simpleName}(get${field.simpleName}() | flags)"))
|
||||
|
||||
val mRemove = mAdd.clone()
|
||||
mRemove.setSimpleName<Nothing>("remove${field.simpleName}")
|
||||
mRemove.setBody<Nothing>(field.factory.createCodeSnippet("set${field.simpleName}(get${field.simpleName}() & ~(flags))"))
|
||||
|
||||
val mHas = mAdd.clone()
|
||||
mHas.setSimpleName<Nothing>("has${field.simpleName}")
|
||||
mHas.setType<Nothing>(field.factory.createTypeParam("boolean"))
|
||||
mHas.setBody<Nothing>(field.factory.createCodeSnippet("return (get${field.simpleName}() & flags) != 0"))
|
||||
|
||||
val utilMethods = mutableListOf<String>()
|
||||
val bindingFieldAnnotation = field.getAnnotation(A_NAME_BINDING_FIELD)
|
||||
|
||||
if (bindingFieldAnnotation?.containsValue(A_VALUE_ACCESSORS, A_TYPE_VALUE_ACCESSOR_SETTER) == true) {
|
||||
utilMethods += mAdd.prettyprint()
|
||||
utilMethods += mRemove.prettyprint()
|
||||
}
|
||||
|
||||
if (bindingFieldAnnotation?.containsValue(A_VALUE_ACCESSORS, A_TYPE_VALUE_ACCESSOR_GETTER) == true) {
|
||||
utilMethods += mHas.prettyprint()
|
||||
}
|
||||
|
||||
return utilMethods
|
||||
}
|
||||
|
||||
fun jvmFieldContent(field: CtField<*>): List<String> {
|
||||
val fields = mutableListOf<String>()
|
||||
val a = field.getAnnotation(A_NAME_BINDING_FIELD)
|
||||
if (a?.containsValue(A_VALUE_ACCESSORS, A_TYPE_VALUE_ACCESSOR_GETTER) == true) {
|
||||
fields += createFieldGetContent(field)
|
||||
}
|
||||
if (a?.containsValue(A_VALUE_ACCESSORS, A_TYPE_VALUE_ACCESSOR_SETTER) == true) {
|
||||
fields += createFieldSetContent(field)
|
||||
}
|
||||
if (field.getAnnotation(A_NAME_BINDING_FIELD)?.getValueAsObject(A_VALUE_IS_FLAG) == true) {
|
||||
fields += createFieldFlagUtils(field)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun getJDoc(method: CtMethod<*>): CtJavaDoc? {
|
||||
return method.comments.filterIsInstance<CtJavaDoc>().firstOrNull()
|
||||
}
|
||||
|
||||
private fun sanitizeDocComment(method: CtMethod<*>) {
|
||||
val jDoc = getJDoc(method) ?: return
|
||||
val paramNames = method.parameters.map { it.simpleName }
|
||||
jDoc.setTags<Nothing>(jDoc.tags.filter { it.type != CtJavaDocTag.TagType.PARAM || paramNames.contains(it.param) })
|
||||
}
|
||||
|
||||
private fun sanitizeAnnotations(method: CtMethod<*>) {
|
||||
method.setAnnotations<Nothing>(method.annotations.filter { !CLEANUP_ANNOTATIONS_LIST.contains(it.name) })
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package tool.generator.api.task
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import spoon.Launcher
|
||||
import spoon.support.compiler.FileSystemFolder
|
||||
import tool.generator.api.BindingSourceProcessor
|
||||
import tool.generator.api.ExcludedSourceProcessor
|
||||
|
||||
open class GenerateApi : DefaultTask() {
|
||||
@Internal
|
||||
override fun getGroup() = "build"
|
||||
|
||||
@Internal
|
||||
override fun getDescription() = "Generate API for native binaries."
|
||||
|
||||
private val genSrcDir = project.file("src/main/java")
|
||||
private val genDstDir = project.file("src/generated/java")
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
logger.info("Generating API...")
|
||||
|
||||
logger.info("Removing old generated files in $genDstDir...")
|
||||
project.file(genDstDir).deleteRecursively()
|
||||
|
||||
logger.info("Copying raw sources...")
|
||||
logger.info("| from: $genSrcDir")
|
||||
logger.info("| into: $genDstDir")
|
||||
project.copy {
|
||||
from(genSrcDir)
|
||||
into(genDstDir)
|
||||
}
|
||||
|
||||
logger.info("Processing generated sources...")
|
||||
|
||||
val launcher = Launcher()
|
||||
launcher.addInputResource(FileSystemFolder(genDstDir))
|
||||
val model = launcher.buildModel()
|
||||
model.allTypes.filter(BindingSourceProcessor::isProcessable).forEach {
|
||||
BindingSourceProcessor(it).process()
|
||||
}
|
||||
model.allTypes.filter(ExcludedSourceProcessor::isProcessable).forEach {
|
||||
ExcludedSourceProcessor(it).process()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package tool.generator.api
|
||||
|
||||
import spoon.reflect.code.CtCodeSnippetStatement
|
||||
import spoon.reflect.code.CtExpression
|
||||
import spoon.reflect.code.CtFieldRead
|
||||
import spoon.reflect.code.CtNewArray
|
||||
import spoon.reflect.declaration.*
|
||||
import spoon.reflect.factory.Factory
|
||||
import spoon.reflect.reference.CtTypeParameterReference
|
||||
import tool.generator.ast.Decl
|
||||
import tool.generator.ast.DeclContainer
|
||||
|
||||
const val A_NAME_BINDING_SOURCE = "BindingSource"
|
||||
const val A_NAME_BINDING_METHOD = "BindingMethod"
|
||||
const val A_NAME_BINDING_FIELD = "BindingField"
|
||||
const val A_NAME_BINDING_AST_ENUM = "BindingAstEnum"
|
||||
const val A_NAME_OPT_ARG = "OptArg"
|
||||
const val A_NAME_EXCLUDED_SOURCE = "ExcludedSource"
|
||||
const val A_NAME_RETURN_VALUE = "ReturnValue"
|
||||
const val A_NAME_ARG_VALUE = "ArgValue"
|
||||
const val A_NAME_TYPE_ARRAY = "TypeArray"
|
||||
|
||||
const val A_VALUE_CALL_PTR = "callPtr"
|
||||
const val A_VALUE_CALL_OPERATOR = "callOperator"
|
||||
const val A_VALUE_CALL_PREFIX = "callPrefix"
|
||||
const val A_VALUE_CALL_SUFFIX = "callSuffix"
|
||||
const val A_VALUE_STATIC_CAST = "staticCast"
|
||||
const val A_VALUE_REINTERPRET_CAST = "reinterpretCast"
|
||||
const val A_VALUE_IS_STATIC = "isStatic"
|
||||
const val A_VALUE_CALL_VALUE = "callValue"
|
||||
const val A_VALUE_NAME = "name"
|
||||
const val A_VALUE_CALL_NAME = "callName"
|
||||
const val A_VALUE_ACCESSORS = "accessors"
|
||||
const val A_VALUE_IS_FLAG = "isFlag"
|
||||
const val A_VALUE_TYPE = "type"
|
||||
const val A_VALUE_SIZE = "size"
|
||||
const val A_VALUE_FILE = "file"
|
||||
const val A_VALUE_QUAL_TYPE = "qualType"
|
||||
const val A_VALUE_SANITIZE_NAME = "sanitizeName"
|
||||
|
||||
const val A_TYPE_VALUE_ACCESSOR_SETTER = "SETTER"
|
||||
const val A_TYPE_VALUE_ACCESSOR_GETTER = "GETTER"
|
||||
|
||||
const val PTR_JVM_FIELD = "ptr"
|
||||
const val PTR_JNI_CAST = "intptr_t"
|
||||
const val PTR_JNI_THIS = "THIS"
|
||||
|
||||
const val PARAM_ARR_LEN_POSTFIX = "Count"
|
||||
|
||||
val CLEANUP_ANNOTATIONS_LIST = listOf(
|
||||
A_NAME_BINDING_SOURCE,
|
||||
A_NAME_BINDING_METHOD,
|
||||
A_NAME_BINDING_FIELD,
|
||||
A_NAME_RETURN_VALUE,
|
||||
A_NAME_OPT_ARG,
|
||||
A_NAME_ARG_VALUE,
|
||||
A_NAME_TYPE_ARRAY,
|
||||
A_NAME_BINDING_AST_ENUM,
|
||||
)
|
||||
|
||||
val PRIMITIVE_PTR_TYPECAST_MAP = mapOf(
|
||||
"boolean[]" to "bool*",
|
||||
"byte[]" to "char*",
|
||||
"short[]" to "short*",
|
||||
"int[]" to "int*",
|
||||
"float[]" to "float*",
|
||||
"long[]" to "long*",
|
||||
"double[]" to "double*",
|
||||
)
|
||||
|
||||
val DST_RETURN_TYPE_SET = setOf(
|
||||
"ImVec2",
|
||||
"ImVec4",
|
||||
"ImRect",
|
||||
"ImPlotPoint",
|
||||
"ImPlotRange",
|
||||
"ImPlotLimits",
|
||||
"TextEditorCoordinates",
|
||||
)
|
||||
|
||||
fun CtElement.hasAnnotation(annotationName: String): Boolean {
|
||||
return getAnnotation(annotationName) != null
|
||||
}
|
||||
|
||||
fun CtElement.getAnnotation(annotationName: String): CtAnnotation<*>? {
|
||||
return annotations.find { a -> a?.name == annotationName }
|
||||
}
|
||||
|
||||
fun CtAnnotation<*>.containsValue(annotationField: String, value: String): Boolean {
|
||||
val v = getValue<CtExpression<Any>>(annotationField)
|
||||
if (v is CtFieldRead) {
|
||||
return v.prettyprint().contains(value)
|
||||
}
|
||||
if (v is CtNewArray) {
|
||||
return v.elements.find { it.prettyprint().contains(value) } != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun Factory.createTypeParam(name: String): CtTypeParameterReference = createTypeParameterReference().apply {
|
||||
setSimpleName<Nothing>(name)
|
||||
}
|
||||
|
||||
fun Factory.createCodeSnippet(code: String): CtCodeSnippetStatement = createCodeSnippetStatement().apply {
|
||||
setValue<Nothing>(code)
|
||||
}
|
||||
|
||||
fun CtTypedElement<*>.isType(type: String): Boolean = this.type.simpleName == type
|
||||
|
||||
fun CtTypedElement<*>.isPrimitivePtrType(): Boolean {
|
||||
return PRIMITIVE_PTR_TYPECAST_MAP.containsKey(type.simpleName)
|
||||
}
|
||||
|
||||
fun CtTypeInformation.isPtrClass(): Boolean {
|
||||
return getDeclaredOrInheritedField(PTR_JVM_FIELD) != null
|
||||
}
|
||||
|
||||
fun CtMethod<*>.getJniName(): String = "n${simpleName.capitalize()}"
|
||||
|
||||
fun CtMethod<*>.getName(): String {
|
||||
return getAnnotation(A_NAME_BINDING_METHOD)?.getValueAsString(A_VALUE_NAME)?.takeIf(String::isNotEmpty)
|
||||
?: simpleName.decapitalize()
|
||||
}
|
||||
|
||||
fun CtField<*>.getCallName(): String {
|
||||
return getAnnotation(A_NAME_BINDING_FIELD)?.getValueAsString(A_VALUE_CALL_NAME)?.takeIf(String::isNotEmpty)
|
||||
?: simpleName
|
||||
}
|
||||
|
||||
fun findDefaultsCombinations(method: CtMethod<*>): List<IntArray> {
|
||||
fun findDefaults(params: List<CtParameter<*>>): Map<Int, String> {
|
||||
val defaults = mutableMapOf<Int, String>()
|
||||
for ((index, p) in params.withIndex()) {
|
||||
p.getAnnotation(A_NAME_OPT_ARG)?.let { a ->
|
||||
val value = a.getValueAsString(A_VALUE_CALL_VALUE)
|
||||
if (value.isNotEmpty()) {
|
||||
defaults[index] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
fun concatNextParam0(combinations: MutableList<IntArray>, idx0: Int, idxN: Int) {
|
||||
val p1 = method.parameters[idx0]
|
||||
val p2 = method.parameters[idxN]
|
||||
if (p1.type.simpleName != p2.type.simpleName) {
|
||||
combinations.add(((0 until idx0) + (idxN until method.parameters.size)).toIntArray())
|
||||
if (p2.getAnnotation(A_NAME_OPT_ARG)?.getValueAsString(A_VALUE_CALL_VALUE)?.isNotEmpty() == true) {
|
||||
concatNextParam0(combinations, idx0, idxN + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val combinations = mutableListOf<IntArray>()
|
||||
findDefaults(method.parameters).forEach { (idx, _) -> concatNextParam0(combinations, idx, idx + 1) }
|
||||
return combinations
|
||||
}
|
||||
|
||||
fun findFirstOptParam(method: CtMethod<*>): Int {
|
||||
if (method.parameters.isEmpty()) {
|
||||
return method.parameters.size
|
||||
}
|
||||
for ((index, p) in method.parameters.withIndex()) {
|
||||
if (p.hasAnnotation(A_NAME_OPT_ARG)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return method.parameters.size
|
||||
}
|
||||
|
||||
fun Collection<Decl>.filterDecls0(filter: (Decl) -> Boolean): Collection<Decl> {
|
||||
val result = mutableListOf<Decl>()
|
||||
fun filter0(decls: Collection<Decl>) {
|
||||
decls.forEach { decl ->
|
||||
if (filter(decl)) {
|
||||
result += decl
|
||||
}
|
||||
if (decl is DeclContainer) {
|
||||
filter0(decl.decls)
|
||||
}
|
||||
}
|
||||
}
|
||||
filter0(this)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package tool.generator.ast
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import java.io.InputStream
|
||||
|
||||
object AstParser {
|
||||
const val RESOURCE_PATH: String = "generator/api/ast"
|
||||
|
||||
private val astCache = mutableMapOf<String, AstRoot>()
|
||||
|
||||
fun readFromResource(astResourcePath: String): AstRoot {
|
||||
return astCache.computeIfAbsent(astResourcePath) {
|
||||
val astResourceContent = getAtResourceStream(astResourcePath).use { s -> s.reader().use { r -> r.readText() } }
|
||||
val objectMapper = ObjectMapper()
|
||||
objectMapper.readValue(astResourceContent, AstRoot::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAtResourceStream(astResourcePath: String): InputStream {
|
||||
return AstRoot::class.java.classLoader.getResourceAsStream("$RESOURCE_PATH/$astResourcePath")!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package tool.generator.ast
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
|
||||
@JsonSubTypes(
|
||||
JsonSubTypes.Type(AstNamespaceDecl::class),
|
||||
JsonSubTypes.Type(AstFullComment::class),
|
||||
JsonSubTypes.Type(AstParagraphComment::class),
|
||||
JsonSubTypes.Type(AstTextComment::class),
|
||||
JsonSubTypes.Type(AstFunctionDecl::class),
|
||||
JsonSubTypes.Type(AstParmVarDecl::class),
|
||||
JsonSubTypes.Type(AstEnumDecl::class),
|
||||
JsonSubTypes.Type(AstEnumConstantDecl::class),
|
||||
JsonSubTypes.Type(AstRecordDecl::class),
|
||||
JsonSubTypes.Type(AstFieldDecl::class),
|
||||
)
|
||||
interface Decl {
|
||||
val offset: Int
|
||||
}
|
||||
|
||||
interface DeclContainer {
|
||||
val decls: List<Decl>
|
||||
}
|
||||
|
||||
data class AstRoot(
|
||||
val info: AstInfo = AstInfo(),
|
||||
override val decls: List<Decl> = emptyList()
|
||||
) : DeclContainer
|
||||
|
||||
data class AstInfo(
|
||||
val source: String = "",
|
||||
val hash: String = "",
|
||||
val url: String = "",
|
||||
val revision: String = "",
|
||||
)
|
||||
|
||||
data class AstNamespaceDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
override val decls: List<Decl> = emptyList(),
|
||||
) : Decl, DeclContainer
|
||||
|
||||
data class AstFullComment(
|
||||
override val offset: Int = -1,
|
||||
override val decls: List<Decl> = emptyList(),
|
||||
) : Decl, DeclContainer
|
||||
|
||||
data class AstParagraphComment(
|
||||
override val offset: Int = -1,
|
||||
override val decls: List<Decl> = emptyList(),
|
||||
) : Decl, DeclContainer
|
||||
|
||||
data class AstTextComment(
|
||||
override val offset: Int = -1,
|
||||
val text: String = "",
|
||||
) : Decl
|
||||
|
||||
data class AstFunctionDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
val resultType: String = "",
|
||||
override val decls: List<Decl> = emptyList(),
|
||||
) : Decl, DeclContainer {
|
||||
@JsonIgnore
|
||||
fun getParams(): List<AstParmVarDecl> {
|
||||
return decls.filterIsInstance<AstParmVarDecl>()
|
||||
}
|
||||
}
|
||||
|
||||
data class AstParmVarDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
val qualType: String = "",
|
||||
val desugaredQualType: String = "",
|
||||
val defaultValue: String? = null,
|
||||
) : Decl {
|
||||
companion object {
|
||||
const val FORMAT_ATTR_NAME = "#FORMAT_ATTR_MARKER#"
|
||||
|
||||
fun asFormatAttr(offset: Int): AstParmVarDecl {
|
||||
return AstParmVarDecl(
|
||||
offset = offset,
|
||||
name = FORMAT_ATTR_NAME,
|
||||
qualType = FORMAT_ATTR_NAME,
|
||||
desugaredQualType = FORMAT_ATTR_NAME,
|
||||
defaultValue = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun isFormatAttr(): Boolean {
|
||||
return this.name == FORMAT_ATTR_NAME
|
||||
}
|
||||
}
|
||||
|
||||
data class AstEnumDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
override val decls: List<Decl> = emptyList(),
|
||||
) : Decl, DeclContainer
|
||||
|
||||
data class AstEnumConstantDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
val docComment: String? = null,
|
||||
val qualType: String = "",
|
||||
val order: Int = -1,
|
||||
val value: String? = null,
|
||||
val evaluatedValue: String? = null,
|
||||
) : Decl
|
||||
|
||||
data class AstRecordDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
override val decls: List<Decl> = emptyList(),
|
||||
) : Decl, DeclContainer
|
||||
|
||||
data class AstFieldDecl(
|
||||
override val offset: Int = -1,
|
||||
val name: String = "",
|
||||
val qualType: String = "",
|
||||
val desugaredQualType: String = "",
|
||||
) : Decl
|
||||
@@ -0,0 +1,530 @@
|
||||
package tool.generator.ast.task
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.lordcodes.turtle.shellRun
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import tool.generator.ast.*
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
open class GenerateAst : DefaultTask() {
|
||||
@Internal
|
||||
override fun getGroup() = "build"
|
||||
|
||||
@Internal
|
||||
override fun getDescription() = "Generate AST tree for declared header files."
|
||||
|
||||
@InputFiles
|
||||
lateinit var headerFiles: Map<File, Collection<String>>
|
||||
|
||||
private val dstDir: File = File("${project.rootDir}/buildSrc/src/main/resources/${AstParser.RESOURCE_PATH}")
|
||||
|
||||
private val objectMapper: ObjectMapper = ObjectMapper()
|
||||
|
||||
/**
|
||||
* During the parsing of ast-dump we need to access an initial header file content.
|
||||
* Required for cases like: get the content of the file (comment or default param value) by offset pos in file.
|
||||
*/
|
||||
private var currentParsingHeaderContent: String = ""
|
||||
|
||||
/**
|
||||
* This is used only to log stuff properly.
|
||||
*/
|
||||
private var currentParsingIndent: Int = 0
|
||||
|
||||
init {
|
||||
objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true)
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
logger.info("Generating AST...")
|
||||
|
||||
dstDir.mkdirs()
|
||||
|
||||
val scriptPath = "$dstDir/_gen_ast.sh"
|
||||
File(scriptPath).setExecutable(true)
|
||||
|
||||
logger.info("Processing headers...")
|
||||
|
||||
headerFiles.forEach { (header, defines) ->
|
||||
System.gc()
|
||||
|
||||
logger.info("| $header")
|
||||
|
||||
// Store the header content in the field, so we can access it when parsing.
|
||||
currentParsingHeaderContent = header.readText()
|
||||
|
||||
// Header hash content to create a unique path for generated content.
|
||||
val headerHash = md5Hash(currentParsingHeaderContent)
|
||||
|
||||
// Append additional defines if provided.
|
||||
currentParsingHeaderContent = defines.joinToString("\n") + currentParsingHeaderContent
|
||||
|
||||
// We create a unique folder to store files generated for the header.
|
||||
// This is necessary because when we store all headers in the same directory,
|
||||
// their AST dump can overlap with each other.
|
||||
val astBuildDir = File("${project.buildDir}/generated/ast/$headerHash")
|
||||
astBuildDir.mkdirs()
|
||||
|
||||
// Move the header content to the temp file to make an ast-dump of it.
|
||||
val headerName = header.nameWithoutExtension
|
||||
val tmpHeaderFile = File("$astBuildDir/$headerName.h")
|
||||
tmpHeaderFile.createNewFile()
|
||||
tmpHeaderFile.writeText(currentParsingHeaderContent)
|
||||
|
||||
logger.info(" | Making an ast-dump...")
|
||||
|
||||
// Destination for ast-dump.
|
||||
val astBumpJson = File("$astBuildDir/$headerName.json")
|
||||
|
||||
// Call clang++ with the script.
|
||||
// During the process of making an ast-dump there could be errors/warnings.
|
||||
// Thus making a call like that can help to ignore them.
|
||||
callClangAstBump(scriptPath, tmpHeaderFile, astBumpJson)
|
||||
|
||||
logger.info(" | Processing an ast-dump result: $astBumpJson...")
|
||||
|
||||
// Read the ast-dump of the whole header content.
|
||||
// It will contain technical information, like unneeded typdefs, std types etc.
|
||||
val fullDeclsList = mutableListOf<Decl>()
|
||||
objectMapper.readTree(astBumpJson).get("inner").forEach { topDecl ->
|
||||
parseDeclNode0(topDecl)?.let(fullDeclsList::add)
|
||||
}
|
||||
|
||||
//////////////
|
||||
// After parsing the entire header file, we need to remove unnecessary information from it.
|
||||
// So we pre-process the initial content and parse it again.
|
||||
// This way, we can compare the initial result with the trimmed version
|
||||
// to remove types and declarations that represent compiler information.
|
||||
|
||||
|
||||
// Remove all includes and typedefs.
|
||||
// They are not needed to process the AST, but they add extra information to the final dump.
|
||||
// And since we want to have an ast-dump as minimal as possible we remove that information.
|
||||
currentParsingHeaderContent = currentParsingHeaderContent
|
||||
.replace("#include .*".toRegex(), "")
|
||||
.replace("typedef .*".toRegex(), "")
|
||||
|
||||
// Re-write with pre-processed content.
|
||||
tmpHeaderFile.writeText(currentParsingHeaderContent)
|
||||
|
||||
callClangAstBump(scriptPath, tmpHeaderFile, astBumpJson)
|
||||
|
||||
// As its said - we do the parsing once again, but now the final result won't contain std types.
|
||||
val strippedDeclsList = mutableListOf<Decl>()
|
||||
objectMapper.readTree(astBumpJson).get("inner").forEach { topDecl ->
|
||||
parseDeclNode0(topDecl)?.let(strippedDeclsList::add)
|
||||
}
|
||||
|
||||
logger.info(" | Removing an ast-decls diff...")
|
||||
val resultDeclsList = removeAstDiff(fullDeclsList, strippedDeclsList)
|
||||
|
||||
logger.info(" | Sorting an ast-decls...")
|
||||
sortDecls0(resultDeclsList)
|
||||
|
||||
///////////////
|
||||
// In the end we write down the ast-decls result to the file.
|
||||
// The file itself will be stored in VCS, so we can understand if there were any changes in native code.
|
||||
|
||||
val astResultJson = File("$dstDir/ast-$headerName.json")
|
||||
|
||||
logger.info(" | Writing processed AST: $astResultJson...")
|
||||
|
||||
objectMapper.writer().writeValue(
|
||||
astResultJson, AstRoot(
|
||||
info = AstInfo(
|
||||
source = header.relativeTo(project.rootDir).path,
|
||||
hash = headerHash,
|
||||
url = shellRun(header.parentFile) {
|
||||
command("git", listOf("config", "--get", "remote.origin.url"))
|
||||
},
|
||||
revision = shellRun(header.parentFile) {
|
||||
command("git", listOf("rev-parse", "HEAD"))
|
||||
},
|
||||
),
|
||||
decls = resultDeclsList
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDeclNode0(declNode: JsonNode): Decl? {
|
||||
fun logParsingDecl(name: String) {
|
||||
logger.info(" ${" ".repeat(currentParsingIndent)}| Parsing $name...")
|
||||
}
|
||||
|
||||
fun logParsingInner(name: String) {
|
||||
logger.info(" ${" ".repeat(currentParsingIndent)}| $name")
|
||||
}
|
||||
|
||||
fun JsonNode.hasNoName(): Boolean {
|
||||
return !has("name") || get("name").asText().isBlank()
|
||||
}
|
||||
|
||||
fun JsonNode.hasNoInnerContent(): Boolean {
|
||||
return !has("inner") || get("inner").isEmpty
|
||||
}
|
||||
|
||||
fun JsonNode.getOffset(): Int {
|
||||
val loc = (get("loc") ?: get("range").get("begin")).let { loc ->
|
||||
if (loc.has("expansionLoc")) {
|
||||
loc.get("expansionLoc")
|
||||
} else {
|
||||
loc
|
||||
}
|
||||
}
|
||||
return loc.get("offset")?.asInt() ?: -1
|
||||
}
|
||||
|
||||
fun JsonNode.getDefaultParamValue(): String {
|
||||
fun getOffset(jsonNode: JsonNode): Pair<Int, Int> {
|
||||
val loc = if (jsonNode.has("expansionLoc")) {
|
||||
jsonNode.get("expansionLoc")
|
||||
} else {
|
||||
jsonNode
|
||||
}
|
||||
return loc.get("offset").asInt() to loc.get("tokLen").asInt()
|
||||
}
|
||||
return get("inner").get(0).get("range").let { range ->
|
||||
val (beginOffset, _) = getOffset(range.get("begin"))
|
||||
val (endOffset, endTokLoc) = getOffset(range.get("end"))
|
||||
currentParsingHeaderContent.substring(beginOffset, endOffset + endTokLoc)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return when (declNode.get("kind").textValue()) {
|
||||
"NamespaceDecl" -> {
|
||||
if (declNode.hasNoInnerContent()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val decls = mutableListOf<Decl>()
|
||||
|
||||
if (name.startsWith("_")) { // internal API namespaces
|
||||
return null
|
||||
}
|
||||
|
||||
logParsingDecl("namespace $name")
|
||||
|
||||
currentParsingIndent++
|
||||
declNode.get("inner").forEach { innerDecl ->
|
||||
parseDeclNode0(innerDecl)?.let(decls::add)
|
||||
}
|
||||
currentParsingIndent--
|
||||
|
||||
return AstNamespaceDecl(offset, name, decls)
|
||||
}
|
||||
|
||||
"FullComment" -> {
|
||||
if (declNode.hasNoInnerContent()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val decls = mutableListOf<Decl>()
|
||||
|
||||
logParsingDecl("full comment")
|
||||
|
||||
currentParsingIndent++
|
||||
declNode.get("inner").forEach { innerDecl ->
|
||||
parseDeclNode0(innerDecl)?.let(decls::add)
|
||||
}
|
||||
currentParsingIndent--
|
||||
|
||||
return AstFullComment(offset, decls)
|
||||
}
|
||||
|
||||
"ParagraphComment" -> {
|
||||
if (declNode.hasNoInnerContent()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val decls = mutableListOf<Decl>()
|
||||
|
||||
logParsingDecl("paragraph comment")
|
||||
|
||||
currentParsingIndent++
|
||||
declNode.get("inner").forEach { innerDecl ->
|
||||
parseDeclNode0(innerDecl)?.let(decls::add)
|
||||
}
|
||||
currentParsingIndent--
|
||||
|
||||
return AstParagraphComment(offset, decls)
|
||||
}
|
||||
|
||||
"TextComment" -> {
|
||||
val offset = declNode.getOffset()
|
||||
val text = declNode.get("text").asText()
|
||||
logParsingInner("text comment")
|
||||
return AstTextComment(offset, text)
|
||||
}
|
||||
|
||||
"FunctionDecl", "CXXMethodDecl" -> {
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val resultType = declNode.get("type").get("qualType").asText().substringBefore("(").trim()
|
||||
val decls = mutableListOf<Decl>()
|
||||
|
||||
logParsingDecl("function $name")
|
||||
|
||||
currentParsingIndent++
|
||||
declNode.get("inner")?.forEach { innerDecl ->
|
||||
parseDeclNode0(innerDecl)?.let(decls::add)
|
||||
}
|
||||
currentParsingIndent--
|
||||
|
||||
return AstFunctionDecl(offset, name, resultType, decls)
|
||||
}
|
||||
|
||||
"ParmVarDecl" -> {
|
||||
if (declNode.hasNoName()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val qualType = declNode.get("type").get("qualType").asText()
|
||||
val desugaredQualType = declNode.get("type").get("desugaredQualType")?.asText() ?: qualType
|
||||
|
||||
val defaultValue: String? = if (declNode.has("init")) {
|
||||
declNode.getDefaultParamValue()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
logParsingInner("param $name")
|
||||
|
||||
return AstParmVarDecl(offset, name, qualType, desugaredQualType, defaultValue)
|
||||
}
|
||||
|
||||
"FormatAttr" -> {
|
||||
logParsingInner("param ...")
|
||||
return AstParmVarDecl.asFormatAttr(declNode.getOffset())
|
||||
}
|
||||
|
||||
"EnumDecl" -> {
|
||||
if (declNode.hasNoInnerContent() || declNode.hasNoName()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val decls = mutableListOf<Decl>()
|
||||
|
||||
logParsingDecl("enum $name")
|
||||
|
||||
currentParsingIndent++
|
||||
declNode.get("inner").forEach { innerDecl ->
|
||||
parseDeclNode0(innerDecl)?.let(decls::add)
|
||||
}
|
||||
currentParsingIndent
|
||||
|
||||
// Sort enums by their real pos in the header file.
|
||||
sortDecls0(decls)
|
||||
|
||||
var order = 0
|
||||
val sortedDecls = decls.map { decl ->
|
||||
if (decl is AstEnumConstantDecl) {
|
||||
if (order == 0 && decl.value != null) {
|
||||
order = decl.value.toIntOrNull() ?: 0
|
||||
}
|
||||
|
||||
AstEnumConstantDecl(
|
||||
decl.offset,
|
||||
decl.name,
|
||||
decl.docComment,
|
||||
decl.qualType,
|
||||
order++,
|
||||
decl.value,
|
||||
decl.evaluatedValue
|
||||
)
|
||||
} else {
|
||||
decl
|
||||
}
|
||||
}
|
||||
|
||||
return AstEnumDecl(offset, name, sortedDecls)
|
||||
}
|
||||
|
||||
"EnumConstantDecl" -> {
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val qualType = declNode.get("type").get("qualType").asText()
|
||||
|
||||
// ->inner[0]->
|
||||
val declValue: String? = declNode.get("inner")?.get(0)?.let {
|
||||
// Check if the first node is not a comment.
|
||||
if (it.get("kind").asText() != "FullComment") {
|
||||
declNode.getDefaultParamValue()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val evaluatedValue: String? = findNode0(declNode, "kind", "ConstantExpr")?.get("value")?.asText()
|
||||
val docComment: String? = declNode.findValuesAsText("text")?.joinToString(" ") { it.trim() }
|
||||
|
||||
logParsingInner("enum value $name")
|
||||
|
||||
return AstEnumConstantDecl(
|
||||
offset,
|
||||
name,
|
||||
docComment,
|
||||
qualType,
|
||||
// We provide a proper order in EnumDecl, after sorting all enums by their offset.
|
||||
-1,
|
||||
declValue,
|
||||
evaluatedValue
|
||||
)
|
||||
}
|
||||
|
||||
"CXXRecordDecl" -> {
|
||||
if (declNode.hasNoInnerContent() || declNode.hasNoName()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val decls = mutableListOf<Decl>()
|
||||
|
||||
logParsingDecl("record $name")
|
||||
|
||||
currentParsingIndent++
|
||||
declNode.get("inner").forEach { innerDecl ->
|
||||
parseDeclNode0(innerDecl)?.let(decls::add)
|
||||
}
|
||||
currentParsingIndent--
|
||||
|
||||
return AstRecordDecl(offset, name, decls)
|
||||
}
|
||||
|
||||
"FieldDecl" -> {
|
||||
if (declNode.hasNoName()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val offset = declNode.getOffset()
|
||||
val name = declNode.get("name").asText()
|
||||
val qualType = declNode.get("type").get("qualType").asText()
|
||||
val desugaredQualType = declNode.get("type").get("desugaredQualType")?.asText() ?: qualType
|
||||
|
||||
logParsingInner("field $name")
|
||||
|
||||
return AstFieldDecl(offset, name, qualType, desugaredQualType)
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Unable to parse: {}", declNode)
|
||||
throw Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun md5Hash(str: String): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val bigInt = BigInteger(1, md.digest(str.toByteArray(Charsets.UTF_8)))
|
||||
return String.format("%032x", bigInt)
|
||||
}
|
||||
|
||||
private fun callClangAstBump(scriptPath: String, srcHeader: File, dstJson: File) {
|
||||
dstJson.delete()
|
||||
val pb = ProcessBuilder()
|
||||
pb.command(scriptPath, srcHeader.absolutePath, dstJson.absolutePath)
|
||||
pb.start().waitFor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Function removes difference between ast-trees.
|
||||
* Basically, it returns the list which contains only types with the same name.
|
||||
*/
|
||||
private fun removeAstDiff(fullDeclsList: List<Decl>, strippedDeclsList: List<Decl>): List<Decl> {
|
||||
fun isSameNamespaceName(d1: Decl, d2: Decl): Boolean {
|
||||
return d1 is AstNamespaceDecl && d2 is AstNamespaceDecl && d1.name == d2.name
|
||||
}
|
||||
|
||||
fun isSameRecordName(d1: Decl, d2: Decl): Boolean {
|
||||
return d1 is AstRecordDecl && d2 is AstRecordDecl && d1.name == d2.name
|
||||
}
|
||||
|
||||
fun isSameEnumName(d1: Decl, d2: Decl): Boolean {
|
||||
return d1 is AstEnumDecl && d2 is AstEnumDecl && d1.name == d2.name
|
||||
}
|
||||
|
||||
fun isSameFunctionName(d1: Decl, d2: Decl): Boolean {
|
||||
return d1 is AstFunctionDecl && d2 is AstFunctionDecl && d1.name == d2.name
|
||||
}
|
||||
|
||||
return fullDeclsList
|
||||
.filter { d1 ->
|
||||
strippedDeclsList
|
||||
.find { d2 ->
|
||||
d1 == d2
|
||||
|| isSameRecordName(d1, d2)
|
||||
|| isSameNamespaceName(d1, d2)
|
||||
|| isSameEnumName(d1, d2)
|
||||
|| isSameFunctionName(d1, d2)
|
||||
} != null
|
||||
}
|
||||
.filterNot { it is AstFunctionDecl && it.name.startsWith("operator") }
|
||||
.toMutableList() // Ensure final list will be mutable for later sorting.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort decls in the provided list recursively using offset value.
|
||||
*/
|
||||
@Suppress("JavaCollectionsStaticMethodOnImmutableList")
|
||||
private fun sortDecls0(decls: List<Decl>) {
|
||||
Collections.sort(decls) { d1, d2 ->
|
||||
d1.offset.compareTo(d2.offset)
|
||||
}
|
||||
decls.forEach {
|
||||
if (it is DeclContainer) {
|
||||
sortDecls0(it.decls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNode0(root: JsonNode, field: String, value: String): JsonNode? {
|
||||
if (root.isObject) {
|
||||
val objectNode = root as ObjectNode
|
||||
val fields = objectNode.fields()
|
||||
while (fields.hasNext()) {
|
||||
val entry = fields.next()
|
||||
if (entry.key == field && entry.value.asText() == value) {
|
||||
return root
|
||||
} else {
|
||||
val foundNode = findNode0(entry.value, field, value)
|
||||
if (foundNode != null) {
|
||||
return foundNode
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (root.isArray) {
|
||||
for (node in root) {
|
||||
val foundNode = findNode0(node, field, value)
|
||||
if (foundNode != null) {
|
||||
return foundNode
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user