WebGL is a big API, it has 200+ properties and 100+ methods, writing a JavaScript stub and corresponding Kotlin stub is very difficult
We need to write 1400+ lines of JavaScript code and 3000+ lines Kotlin code
This Kotlin gradle script provides a simple approach to auto generate the stub and also compile the generated code to .klib
Gradle Stub Code in KTS
val stubgenerator by tasks.creating() {
var workingDir = projectDir
var fields = GL20::class.java.fields
var ktsb:StringBuilder = StringBuilder()
ktsb.append("package webgl\r\n\r\n")
ktsb.append("import kotlinx.wasm.jsinterop.*\r\n\r\n")
ktsb.append("@SymbolName(\"getGLContext\")\r\n")
ktsb.append("external public fun getGLContext(arena: Int, index: Int, contextPtr: Int, contextLen: Int, resultArena: Int): Int\r\n")
var ktsbc:StringBuilder = StringBuilder()
var sb:StringBuilder = StringBuilder()
sb.append("konan.libraries.push ({")
sb.append("""
getGLContext: function(arena, obj, contextPtr, contextLen, resultArena) {
var context = toUTF16String(contextPtr, contextLen);
var result = kotlinObject(arena, obj).getContext(context);
return toArena(resultArena, result);
},
""")
for(f in fields){
var fname = ""
sb.append(" ")
if(f.getName().startsWith("GL_")){
fname = f.getName().substring(3)
}else{
fname = f.getName()
}
ktsb.append("@SymbolName(\"_GL_" + fname + "\")\r\n")
sb.append("_GL_"+fname)
sb.append(": function(arena, obj) {\r\n")
sb.append(" return kotlinObject(arena, obj)." + fname + ";\r\n")
sb.append(" },\r\n")
ktsb.append("external public fun _GL_" + fname + "(arena: Int, index: Int): Int\r\n\r\n")
ktsbc.append(" val GL_"+fname+": Int\r\n")
ktsbc.append(" get() {\r\n")
ktsbc.append(" return _GL_" + fname + "(this.arena, this.index)\r\n");
ktsbc.append(" }\r\n")
}
var methods = GL20::class.java.methods
for(f in methods){
var fname = f.getName()
ktsb.append("@SymbolName(\"_" + fname + "\")\r\n")
sb.append(" _" + fname)
var params = f.getParameters()
var strParams = ""
var ktStrParams = ""
for(p in params){
strParams = strParams + p.getName() + ", "
ktStrParams = ktStrParams + p.getName() +":"
if(p.getType().getName().toLowerCase().contains("int"))
ktStrParams = ktStrParams + " Int"
else if(p.getType().getName().toLowerCase().contains("float"))
ktStrParams = ktStrParams + " Float"
else
ktStrParams = ktStrParams + " JsValue"
ktStrParams = ktStrParams+ ","
}
if(!strParams.trim().equals(""))
strParams = strParams.substring(0,strParams.length-2)
if(!ktStrParams.trim().equals(""))
ktStrParams = ktStrParams.substring(0,ktStrParams.length-1)
sb.append(": function(arena, obj " + (if (strParams.trim().equals("")) "" else ", "+strParams) + ") {\r\n")
sb.append(" kotlinObject(arena, obj)." + (fname.substring(2,3).toLowerCase() + fname.substring(3)) + "(" + strParams + ");\r\n")
sb.append(" },\r\n")
ktsbc.append(" fun "+fname+"(" + ktStrParams + "):")
ktsb.append("external public fun _" + fname + "(arena: Int, index: Int" + (if (ktStrParams.trim().equals("")) "" else ", "+ktStrParams) + "): ")
if(f.getReturnType().getName().toLowerCase().contains("void")){
ktsb.append("Unit\r\n\r\n")
ktsbc.append("Unit {\r\n")
ktsbc.append(" _" + fname + "(this.arena, this.index" + (if (strParams.trim().equals("")) "" else ", "+strParams) + ")\r\n")
ktsbc.append(" }\r\n")
}else{
ktsb.append("Int\r\n\r\n")
ktsbc.append("Int {\r\n")
ktsbc.append(" return _" + fname + "(this.arena, this.index" + (if (strParams.trim().equals("")) "" else ", "+strParams) + ")\r\n")
ktsbc.append(" }\r\n")
}
}
sb.deleteCharAt(sb.length-1)
sb.deleteCharAt(sb.length-1)
sb.deleteCharAt(sb.length-1)
sb.append("\r\n})\r\n")
ktsb.append("open class GLContext(arena: Int, index: Int): JsValue(arena, index) {\r\n")
ktsb.append("constructor(jsValue: JsValue): this(jsValue.arena, jsValue.index)\r\n")
ktsb.append(ktsbc)
ktsb.append("}\r\n\r\n")
ktsb.append("""
open class GLCanvas(arena: Int, index: Int): JsValue(arena, index) {
constructor(jsValue: JsValue): this(jsValue.arena, jsValue.index)
fun getContext(context: String): GLContext {
val wasmRetVal = getGLContext(this.arena, this.index, stringPointer(context), stringLengthBytes(context), ArenaManager.currentArena)
return GLContext(ArenaManager.currentArena, wasmRetVal)
}
companion object {
}
}
val JsValue.asGLCanvas: GLCanvas
get() {
return GLCanvas(this.arena, this.index)
}
""")
FileOutputStream(file(projectDir).resolve("src/webglMain/js/stub.js")).write(sb.toString().toByteArray())
FileOutputStream(file(projectDir).resolve("src/webglMain/kotlin/webgl/App.kt")).write(ktsb.toString().toByteArray())
}
Important Note: This gradle code only works for void return methods with numeric parameters, for full fledged stub we need to write more code with advanced logic for buffer and arena and string pointers
Important Note: This gradle code generation works based on Java Reflection by using LibGDX GL20 java code
Kotlin Main Code
package wasm
import kotlinx.interop.wasm.dom.*
import kotlinx.wasm.jsinterop.*
import webgl.*
fun main() {
val glCanvas = document.getElementById("myCanvas").asGLCanvas
val glCtx = glCanvas.getContext("webgl")
with(glCtx) {
glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
glClear(GL_COLOR_BUFFER_BIT)
glEnable(GL_SCISSOR_TEST)
glScissor(20,20,50,50)
glClearColor(0.0f, 1.0f, 0.0f, 1.0f)
glClear(GL_COLOR_BUFFER_BIT)
glScissor(70,70,50,50)
glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
glClear(GL_COLOR_BUFFER_BIT)
glScissor(120,120,50,50)
glClearColor(1.0f, 1.0f, 1.0f, 1.0f)
glClear(GL_COLOR_BUFFER_BIT)
glDisable(GL_SCISSOR_TEST)
}
}
This code very much resembles the legacy C OpenGL code, therefore no learning curve required
WebGL Output
GitHub: https://github.com/dickensas/kotlin-gradle-templates/tree/master/sboot-wasm-webgl
End of Article !