Allow Kotlin coroutines

This method allows clients to use coroutines for async methods by
supplying a helper suspend method for each function.

This provides (IMHO) the ideal API for Kotlin apps and retains the
option to switch to an alternative method using Futures at a later
date.

Bug: 330292651

Change-Id: Iaf9b464e80ea7f3fcf2dfae8c211c722e4a0e311
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/191380
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: dan sinclair <dsinclair@google.com>
Commit-Queue: Jim Blackler <jimblackler@google.com>
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index 90bf762..eb5d614 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -837,6 +837,17 @@
     def jni_name(type):
         return kt_file_path + '/' + type.name.CamelCase()
 
+    # We assume that if the final two parameters are named 'userdata' and 'callback' respectively
+    # that this is an async method that uses function pointer based callbacks.
+    def is_async_method(method):
+        if len(method.arguments) < 3:
+            return False  # Not enough parameters to be an async method.
+        if method.arguments[-1].name.get() != 'userdata':
+            return False
+        if method.arguments[-2].name.get() != 'callback':
+            return False
+        return True
+
     # A structure may need to know which other structures listed it as a chain root, e.g.
     # to know whether to mark the generated class 'open'.
     chain_children = defaultdict(list)
@@ -847,6 +858,7 @@
     params_kotlin['include_structure_member'] = include_structure_member
     params_kotlin['include_method'] = include_method
     params_kotlin['jni_name'] = jni_name
+    params_kotlin['is_async_method'] = is_async_method
     return params_kotlin
 
 
@@ -1504,6 +1516,10 @@
                            'java/' + kt_file_path + '/Functions.kt',
                            [RENDER_PARAMS_BASE, params_kotlin]))
             renders.append(
+                FileRender('art/api_kotlin_async_helpers.kt',
+                           'java/' + kt_file_path + '/AsyncHelpers.kt',
+                           [RENDER_PARAMS_BASE, params_kotlin]))
+            renders.append(
                 FileRender('art/structures.h', 'cpp/structures.h',
                            [RENDER_PARAMS_BASE, params_kotlin]))
             renders.append(
diff --git a/generator/templates/art/api_kotlin_async_helpers.kt b/generator/templates/art/api_kotlin_async_helpers.kt
new file mode 100644
index 0000000..1e1ad89
--- /dev/null
+++ b/generator/templates/art/api_kotlin_async_helpers.kt
@@ -0,0 +1,70 @@
+//* Copyright 2024 The Dawn & Tint Authors
+//*
+//* Redistribution and use in source and binary forms, with or without
+//* modification, are permitted provided that the following conditions are met:
+//*
+//* 1. Redistributions of source code must retain the above copyright notice, this
+//*    list of conditions and the following disclaimer.
+//*
+//* 2. Redistributions in binary form must reproduce the above copyright notice,
+//*    this list of conditions and the following disclaimer in the documentation
+//*    and/or other materials provided with the distribution.
+//*
+//* 3. Neither the name of the copyright holder nor the names of its
+//*    contributors may be used to endorse or promote products derived from
+//*    this software without specific prior written permission.
+//*
+//* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//* 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 }}
+
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+{% from 'art/api_kotlin_types.kt' import kotlin_declaration, kotlin_definition with context %}
+
+//* We make a return class for every function pointer so that usage of callback-using methods can be
+// replaced with suspend (async) function that returns the same data.
+{% for function_pointer
+        in by_category['function pointer'] if len(function_pointer.name.chunks) > 1 %}
+    //* Function pointers generally end in Callback which we replace with Return.
+    {% set return_name = function_pointer.name.chunks[:-1] | map('title') | join + 'Return' %}
+    data class {{ return_name }}(
+        //* Kotlin doesn't need userdata because of captures, so we omit it.
+        {% for arg in function_pointer.arguments if arg.name.get() != 'userdata' %}
+            val {{ as_varName(arg.name) }}: {{ kotlin_declaration(arg) }},
+        {% endfor %})
+{% endfor %}
+
+//* Every method that is identified as using callbacks is given a helper method that wraps the
+//* call with a suspend function.
+{% for obj in by_category['object'] %}
+    {% for method in obj.methods if is_async_method(method) %}
+        {% set function_pointer = method.arguments[-2].type %}
+        {% set return_name = function_pointer.name.chunks[:-1] | map('title') | join + 'Return' %}
+        suspend fun {{ obj.name.CamelCase() }}.{{ method.name.camelCase() }}(
+            {%- for arg in method.arguments[:-2] %}
+                {{- as_varName(arg.name) }}: {{ kotlin_definition(arg) }},
+            {%- endfor %}) = suspendCoroutine {
+                {{ method.name.camelCase() }}(
+                    {%- for arg in method.arguments[:-2] %}
+                        {{- as_varName(arg.name) }},
+                    {% endfor %}) {
+                    {%- for arg in function_pointer.arguments if arg.name.get() != 'userdata' %}
+                        {{- as_varName(arg.name) }},
+                    {%- endfor %} -> it.resume({{ return_name }}(
+                        {%- for arg in function_pointer.arguments if arg.name.get() != 'userdata' %}
+                            {{- as_varName(arg.name) }},
+                        {%- endfor %})
+                    )
+                }
+            }
+    {% endfor %}
+{% endfor %}
diff --git a/tools/android/BUILD.gn b/tools/android/BUILD.gn
index b7caaae..05fb81c 100644
--- a/tools/android/BUILD.gn
+++ b/tools/android/BUILD.gn
@@ -38,6 +38,7 @@
     "java/android/dawn/AdapterProperties.kt",
     "java/android/dawn/AdapterType.kt",
     "java/android/dawn/AddressMode.kt",
+    "java/android/dawn/AsyncHelpers.kt",
     "java/android/dawn/BackendType.kt",
     "java/android/dawn/BindGroup.kt",
     "java/android/dawn/BindGroupDescriptor.kt",