Kotlin: Enable methods that return containers.

Bug: 343642438
Change-Id: Ic438b75b79648807547536809043bab4246548a9
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/196335
Commit-Queue: Jim Blackler <jimblackler@google.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index b1a1b59..8b99c5a 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -832,6 +832,7 @@
         return True
 
     def filter_arguments(arguments):
+        # TODO(b/352047733): Replace methods that require special handling with an exceptions list.
         for argument in arguments:
             # length parameters are omitted because Kotlin containers have 'length'.
             if argument in [arg.length for arg in arguments]:
@@ -841,16 +842,32 @@
             if argument.name.get() == 'userdata':
                 continue
 
+            # Dawn uses 'annotation = *' for output parameters, for example to return arrays.
+            # We convert the return type and strip out the parameters.
+            if argument.annotation == '*':
+                continue
+
             yield argument
 
+    def kotlin_return(method):
+        for argument in method.arguments:
+            if argument.annotation == '*':
+                # TODO(b/352048981): Use handwritten methods for container returns to avoid the need
+                # for special casing logic.
+                if method.return_type.name.get() == 'size_t':
+                    return argument
+
+        return {"type": method.return_type}
+
     def include_method(method):
         if method.return_type.category == 'function pointer':
             # Kotlin doesn't support returning functions.
             return False
         for argument in method.arguments:
-            if argument.annotation == '*':
+            if (argument.annotation == '*'
+                    and method.return_type.name.get() != 'size_t'):
                 # Dawn uses 'annotation = *' for output parameters, for example to return arrays.
-                # Kotlin doesn't support that at the moment
+                # Kotlin doesn't support that at the moment, unless a container is returned.
                 return False
             if argument.type.category == 'callback info':
                 # We don't handle this yet.
@@ -888,6 +905,7 @@
     params_kotlin['chain_children'] = chain_children
     params_kotlin['filter_arguments'] = filter_arguments
     params_kotlin['include_structure_member'] = include_structure_member
+    params_kotlin['kotlin_return'] = kotlin_return
     params_kotlin['include_method'] = include_method
     params_kotlin['jni_name'] = jni_name
     params_kotlin['is_async_method'] = is_async_method
diff --git a/generator/templates/art/api_kotlin_functions.kt b/generator/templates/art/api_kotlin_functions.kt
index 73cabe5..53fee9d 100644
--- a/generator/templates/art/api_kotlin_functions.kt
+++ b/generator/templates/art/api_kotlin_functions.kt
@@ -25,11 +25,11 @@
 //* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 //* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 package {{ kotlin_package }}
-{% from 'art/api_kotlin_types.kt' import kotlin_type_declaration, kotlin_definition with context %}
+{% from 'art/api_kotlin_types.kt' import kotlin_declaration, kotlin_definition with context %}
 
 {% for function in by_category['function'] if include_method(function) %}
     external fun {{ function.name.camelCase() }}(
         {%- for arg in function.arguments -%}
             {{- as_varName(arg.name) }}: {{ kotlin_definition(arg) }},{{' '}}
-        {%- endfor %}): {{ kotlin_type_declaration(function.return_type) -}}
+        {%- endfor %}): {{ kotlin_declaration(kotlin_return(function)) }}
 {% endfor %}
diff --git a/generator/templates/art/api_kotlin_object.kt b/generator/templates/art/api_kotlin_object.kt
index fba4a52..43756b2 100644
--- a/generator/templates/art/api_kotlin_object.kt
+++ b/generator/templates/art/api_kotlin_object.kt
@@ -27,7 +27,7 @@
 package {{ kotlin_package }}
 
 import java.nio.ByteBuffer
-{% from 'art/api_kotlin_types.kt' import kotlin_type_declaration, kotlin_definition with context %}
+{% from 'art/api_kotlin_types.kt' import kotlin_declaration, kotlin_definition with context %}
 
 class {{ obj.name.CamelCase() }}(val handle: Long): AutoCloseable {
     {% for method in obj.methods if include_method(method) %}
@@ -37,7 +37,7 @@
         //* TODO(b/341923892): rework async methods to use futures.
         {%- for arg in filter_arguments(method.arguments) %}
             {{- as_varName(arg.name) }}: {{ kotlin_definition(arg) }},{{ ' ' }}
-        {%- endfor -%}): {{ kotlin_type_declaration(method.return_type) }}
+        {%- endfor -%}): {{ kotlin_declaration(kotlin_return(method)) -}}
         {% if method.name.chunks[0] == 'get' and not method.arguments %}
             //* For the Kotlin getter, strip word 'get' from name and convert the remainder to
             //* camelCase() (lower case first word). E.g. "get foo bar" translated to fooBar.
diff --git a/generator/templates/art/api_kotlin_types.kt b/generator/templates/art/api_kotlin_types.kt
index fa8cad2..5ea6317 100644
--- a/generator/templates/art/api_kotlin_types.kt
+++ b/generator/templates/art/api_kotlin_types.kt
@@ -34,11 +34,18 @@
         {%- if emit_defaults and (default_value or optional) -%}
             = null
         {%- endif %}
-    {%- elif arg.length and arg.constant_length != 1 %}
+    {% elif type.name.get() == 'void' %}
+        {%- if arg.length and arg.constant_length != 1 -%}  {# void with length is binary data #}
+            java.nio.ByteBuffer{{ ' = java.nio.ByteBuffer.allocateDirect(0)' if emit_defaults }}
+        {%- elif arg.annotation == '*' -%}  {# void* is a C handle; converted to Kotlin Long #}
+            Long
+        {%- else -%}
+            Unit  {# raw void is C type for no return type; Kotlin equivalent is Unit #}
+        {%- endif -%}
+    {%- elif (arg.length and arg.constant_length != 1) or arg.annotation == '*' %}
+        {# * annotation can mean an array, e.g. an output argument #}
         {%- if type.category in ['bitmask', 'enum', 'function pointer', 'object', 'structure'] -%}
             Array<{{ type.name.CamelCase() }}>{{ ' = arrayOf()' if emit_defaults }}
-        {%- elif type.name.get() == 'void' -%}
-            java.nio.ByteBuffer{{ ' = java.nio.ByteBuffer.allocateDirect(0)' if emit_defaults }}
         {%- elif type.name.get() in ['int', 'int32_t', 'uint32_t'] -%}
             IntArray{{ ' = intArrayOf()' if emit_defaults }}
         {%- else -%}
@@ -101,8 +108,6 @@
                 = {{ default_value }}
             {%- endif %}
         {% endif %}
-    {%- elif type.name.get() == 'void' %}
-        {{- 'Long' if arg.annotation == '*' else 'Unit' }}
     {%- elif type.name.get() in ['void *', 'void const *'] %}
         ByteBuffer
     {%- else -%}
@@ -118,6 +123,3 @@
     {{ declaration_with_defaults(arg, false) }}
 {%- endmacro %}
 
-{% macro kotlin_type_declaration(type) -%}
-    {{ declaration_with_defaults({"type": type}, false) }}
-{%- endmacro %}
diff --git a/generator/templates/art/methods.cpp b/generator/templates/art/methods.cpp
index c215c45..c5493ee 100644
--- a/generator/templates/art/methods.cpp
+++ b/generator/templates/art/methods.cpp
@@ -26,6 +26,7 @@
 //* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 {% from 'art/api_jni_types.kt' import arg_to_jni_type, to_jni_type with context %}
 #include <jni.h>
+#include <stdlib.h>
 #include "dawn/webgpu.h"
 #include "structures.h"
 
@@ -63,9 +64,10 @@
 }
 
 {% macro render_method(method, object) %}
+    {% set _kotlin_return = kotlin_return(method) %}
     //*  A JNI-external method is built with the JNI signature expected to match the host Kotlin.
     DEFAULT extern "C"
-    {{ to_jni_type(method.return_type) }} Java_{{ kotlin_package.replace('.', '_') }}_
+    {{ arg_to_jni_type(_kotlin_return) }} Java_{{ kotlin_package.replace('.', '_') }}_
             {{- object.name.CamelCase() if object else 'FunctionsKt' -}}
             _{{ method.name.camelCase() }}(JNIEnv *env
                     {{ ', jobject obj' if object else ', jclass clazz' -}}
@@ -227,19 +229,59 @@
     {% endif %}
 
     //* Actually invoke the native version of the method.
-    {{ 'auto result =' if method.return_type.name.get() != 'void' }}
-    {% if object %}
+    {% if _kotlin_return.annotation == '*' %}
+        //* Methods that return containers are converted from two-call to single call, and the
+        //* return type is switched to a Kotlin container.
+        size_t size = wgpu{{ object.name.CamelCase() }}{{ method.name.CamelCase() }}(handle
+            {% for arg in method.arguments -%},
+                //* The replaced output parameter is set to nullptr on the first call.
+                {{ 'nullptr' if arg == _kotlin_return else as_varName(arg.name) -}}
+            {% endfor %}
+        );
+        //* Allocate the native container
+        {{ _kotlin_return.name.get() }} = static_cast<{{ as_cType(_kotlin_return.type.name) }} *>(
+                calloc(sizeof({{ as_cType(_kotlin_return.type.name) }}), size));
+        if (env->ExceptionCheck()) {  //* Early out if client (Kotlin) callback threw an exception.
+            //* TODO(b/330293719): use type that automatically releases resources.
+            free({{ _kotlin_return.name.get() }});
+            return nullptr;
+        }
+        //* Second call completes the native container
         wgpu{{ object.name.CamelCase() }}{{ method.name.CamelCase() }}(handle
+            {% for arg in method.arguments -%}
+                {{- ',' if object or not loop.first }}{{ as_varName(arg.name) -}}
+            {% endfor %}
+        );
+        if (env->ExceptionCheck()) {  //* Early out if client (Kotlin) callback threw an exception.
+            //* TODO(b/330293719): use type that automatically releases resources.
+            free({{ _kotlin_return.name.get() }});
+            return nullptr;
+        }
+        //* Native container converted to a Kotlin container.
+        jclass returnClass = env->FindClass("{{ jni_name(_kotlin_return.type) }}");
+        jobjectArray result = env->NewObjectArray(size, returnClass, 0);
+        auto constructor = env->GetMethodID(returnClass, "<init>", "(I)V");
+        for (int idx = 0; idx != size; idx++) {
+            jobject element = env->NewObject(returnClass, constructor,
+                    static_cast<jint>({{ _kotlin_return.name.get() }}[idx]));
+            env->SetObjectArrayElement(result, idx, element);
+        }
+        free({{ _kotlin_return.name.get() }});
     {% else %}
-        wgpu{{ method.name.CamelCase() }}(
+        {{ 'auto result =' if method.return_type.name.get() != 'void' }}
+        {% if object %}
+            wgpu{{ object.name.CamelCase() }}{{ method.name.CamelCase() }}(handle
+        {% else %}
+            wgpu{{ method.name.CamelCase() }}(
+        {% endif %}
+            {% for arg in method.arguments -%}
+                {{- ',' if object or not loop.first }}{{ as_varName(arg.name) -}}
+            {% endfor %}
+        );
+        if (env->ExceptionCheck()) {  //* Early out if client (Kotlin) callback threw an exception.
+            return {{ '0' if method.return_type.name.get() != 'void' }};
+        }
     {% endif %}
-        {% for arg in method.arguments -%}
-            {{- ',' if object or not loop.first }}{{ as_varName(arg.name) -}}
-        {% endfor %}
-    );
-    if (env->ExceptionCheck()) {  //* Early out if client (Kotlin) callback threw an exception.
-        return {{ '0' if method.return_type.name.get() != 'void' }};
-    }
 
     //* We only handle objects and primitives to be returned.
     {% if method.return_type.category == 'object' %}
diff --git a/tools/android/webgpu/src/androidTest/java/android/dawn/DawnTestLauncher.kt b/tools/android/webgpu/src/androidTest/java/android/dawn/DawnTestLauncher.kt
index 214f747..f91cd40 100644
--- a/tools/android/webgpu/src/androidTest/java/android/dawn/DawnTestLauncher.kt
+++ b/tools/android/webgpu/src/androidTest/java/android/dawn/DawnTestLauncher.kt
@@ -45,4 +45,4 @@
         }
         instance.close()
     }
-}
+}
\ No newline at end of file
diff --git a/tools/android/webgpu/src/androidTest/java/android/dawn/FeaturesTest.kt b/tools/android/webgpu/src/androidTest/java/android/dawn/FeaturesTest.kt
new file mode 100644
index 0000000..d19765f
--- /dev/null
+++ b/tools/android/webgpu/src/androidTest/java/android/dawn/FeaturesTest.kt
@@ -0,0 +1,25 @@
+package android.dawn
+
+import android.dawn.FeatureName
+import android.dawn.dawnTestLauncher
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FeaturesTest {
+    /**
+     * Test that the features requested match the features in the adapter are present on the device.
+     */
+    @Test
+    fun featuresTest() {
+        val requiredFeatures =
+            arrayOf(FeatureName.BGRA8UnormStorage, FeatureName.TextureCompressionASTC)
+        dawnTestLauncher(requiredFeatures) { device ->
+            val deviceFeatures = device.enumerateFeatures()
+            requiredFeatures.forEach {
+                assert(deviceFeatures.contains(it)) { "Requested feature $it available on device" }
+            }
+        }
+    }
+}