Kotlin Web Assembly WebGL Auto Stub

Submitted by Dickens A S on Sat, 05/02/2020 - 05:37

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

GL Scissor demo

GitHub: https://github.com/dickensas/kotlin-gradle-templates/tree/master/sboot-wasm-webgl

End of Article !

 

Add new comment