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:
SpaiR committed 2024-08-08 14:46:23 +03:00
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
}