Kotlin: allow use of new callback info and callback function.

Convert the error handling in the tests to the non-deprecated
method, now that will work.

Also enable handling of legacy callback structures (structures
with names ending 'callback info').

Bug: 373837184, 352710628
Test: ErrorTest.*
Change-Id: Ia4e1cee21d347d0916ffc912b88e0bb2fa7a9281
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/217136
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Jim Blackler <jimblackler@google.com>
Reviewed-by: Alex Benton <bentonian@google.com>
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index 96d700f..82951ae 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -712,11 +712,6 @@
 
     def kotlin_record_members(members):
         for member in members:
-            # Skip over callback infos as we haven't implemented support for them yet.
-            # TODO(352710628) support converting callback info.
-            if member.type.category in ['callback info']:
-                continue
-
             # length parameters are omitted because Kotlin containers have 'length'.
             if member in [m.length for m in members]:
                 continue
@@ -763,9 +758,6 @@
         return True
 
     def include_structure(structure):
-        # TODO(352710628) support converting callback info.
-        if structure.name.canonical_case().endswith(" callback info"):
-            return False
         if structure.name.canonical_case() == "string view":
             return False
         return True
@@ -1431,7 +1423,8 @@
             ]
 
             by_category = params_kotlin['by_category']
-            for structure in by_category['structure']:
+            for structure in by_category['structure'] + by_category[
+                    'callback info']:
                 if structure.name.get() != "string view":
                     renders.append(
                         FileRender('art/api_kotlin_structure.kt',
@@ -1448,7 +1441,8 @@
                         [RENDER_PARAMS_BASE, params_kotlin, {
                             'obj': obj
                         }]))
-            for function_pointer in by_category['function pointer']:
+            for function_pointer in (by_category['function pointer'] +
+                                     by_category['callback function']):
                 renders.append(
                     FileRender('art/api_kotlin_function_pointer.kt',
                                'java/' + jni_name(function_pointer) + '.kt', [
diff --git a/generator/templates/art/api_jni_types.cpp b/generator/templates/art/api_jni_types.cpp
index 4d4e654..c50e890 100644
--- a/generator/templates/art/api_jni_types.cpp
+++ b/generator/templates/art/api_jni_types.cpp
@@ -27,7 +27,7 @@
 
 {% macro arg_to_jni_type(arg) %}
     {%- if arg.length and arg.length != 'constant' -%}
-        {%- if arg.type.category in ['bitmask', 'enum', 'function pointer', 'object', 'structure'] -%}
+        {%- if arg.type.category in ['bitmask', 'callback function', 'enum', 'function pointer', 'object', 'callback info', 'structure'] -%}
             jobjectArray
         {%- elif arg.type.name.get() == 'void' -%}
             jobject
@@ -44,7 +44,7 @@
 {% macro to_jni_type(type) %}
     {%- if type.name.get() == "string view" -%}
         jstring
-    {%- elif type.category in ['function pointer', 'object', 'structure'] -%}
+    {%- elif type.category in ['callback function', 'function pointer', 'object', 'callback info', 'structure'] -%}
         jobject
     {%- elif type.category in ['bitmask', 'enum'] -%}
         jint
@@ -69,7 +69,7 @@
 {% endmacro %}
 
 {% macro jni_signature_single_value(type) %}
-    {%- if type.category in ['function pointer', 'object', 'structure'] -%}
+    {%- if type.category in ['function pointer', 'object', 'callback function', 'callback info', 'structure'] -%}
         L{{ jni_name(type) }};
     {%- elif type.category in ['bitmask', 'enum'] -%}
         {{ jni_signatures['int32_t'] }}//*  JvmInline makes lone bitmask/enums appear as integer to JNI.
@@ -96,7 +96,7 @@
     {% if size is string %}
         {% if member.type.name.get() in ['void const *', 'void *'] %}
             jobject {{ output }} = toByteBuffer(env, {{ input }}, {{ size }});
-        {% elif member.type.category in ['bitmask', 'enum', 'object', 'structure'] %}
+        {% elif member.type.category in ['bitmask', 'enum', 'object', 'callback info', 'structure'] %}
             //* Native container converted to a Kotlin container.
             jobjectArray {{ output }} = env->NewObjectArray(
                     {{ size }},
@@ -118,13 +118,16 @@
             jmethodID init = env->GetMethodID(clz, "<init>", "(J)V");
             {{ output }} = env->NewObject(clz, init, reinterpret_cast<jlong>({{ input }}));
         }
-    {% elif member.type.category == 'structure' %}
+    {% elif member.type.category in ['callback info', 'structure'] %}
         jobject {{ output }} = ToKotlin(env, {{ '&' if member.annotation not in ['*', 'const*'] }}{{ input }});
     {% elif member.type.name.get() == 'void *' %}
         jlong {{ output }} = reinterpret_cast<jlong>({{ input }});
     {% elif member.type.category in ['bitmask', 'enum', 'native'] %}
         //* We use Kotlin value classes for bitmask and enum, and they get inlined as lone values.
         {{ to_jni_type(member.type) }} {{ output }} = static_cast<{{ to_jni_type(member.type) }}>({{ input }});
+    {% elif member.type.category in ['callback function', 'function pointer'] %}
+        jobject {{ output }} = nullptr;
+        dawn::WarningLog() << "while converting {{ as_cType(member.type.name) }}: Native callbacks cannot be converted to Kotlin";
     {% else %}
         {{ unreachable_code() }}
     {% endif %}
diff --git a/generator/templates/art/api_kotlin_types.kt b/generator/templates/art/api_kotlin_types.kt
index f666094..a29cf60 100644
--- a/generator/templates/art/api_kotlin_types.kt
+++ b/generator/templates/art/api_kotlin_types.kt
@@ -42,14 +42,14 @@
         {%- endif -%}
     {%- elif arg.length and arg.length != 'constant' %}
         {# * annotation can mean an array, e.g. an output argument #}
-        {%- if type.category in ['bitmask', 'enum', 'function pointer', 'object', 'structure'] -%}
+        {%- if type.category in ['bitmask', 'callback function', 'callback info', 'enum', 'function pointer', 'object', 'structure'] -%}
             Array<{{ type.name.CamelCase() }}>{{ ' = arrayOf()' if emit_defaults }}
         {%- elif type.name.get() in ['int', 'int32_t', 'uint32_t'] -%}
             IntArray{{ ' = intArrayOf()' if emit_defaults }}
         {%- else -%}
             {{ unreachable_code() }}
         {% endif %}
-    {%- elif type.category in ['function pointer', 'object'] %}
+    {%- elif type.category in ['callback function', 'function pointer', 'object'] %}
         {{- type.name.CamelCase() }}
         {%- if optional or default_value %}?{{ ' = null' if emit_defaults }}{% endif %}
     {%- elif type.category == 'structure' or type.category == 'callback info' %}
diff --git a/generator/templates/art/kotlin_record_conversion.cpp b/generator/templates/art/kotlin_record_conversion.cpp
index 46d799d..c805f64 100644
--- a/generator/templates/art/kotlin_record_conversion.cpp
+++ b/generator/templates/art/kotlin_record_conversion.cpp
@@ -115,7 +115,7 @@
                     } else {
                         out = nullptr;
                     }
-                {% elif member.type.category == 'structure' %}
+                {% elif member.type.category in ['callback info', 'structure'] %}
                     //* Mandatory structure.
                     ToNative(c, env, in, &out);
                 {% elif member.name.get() == "window" and member.type.name.get() == "void *" %}
@@ -125,20 +125,33 @@
                     out = reinterpret_cast<{{as_cType(member.type.name)}}>(static_cast<uintptr_t>(in));
                 {% elif member.type.category in ["native", "enum", "bitmask"] %}
                     out = static_cast<{{as_cType(member.type.name)}}>(in);
-                {% elif member.type.category == 'function pointer' %}
-                    //* Function pointers themselves require each argument converting.
+                {% elif member.type.category in ['callback function', 'function pointer'] %}
+                    //* Function pointers and callback functions require each argument converting.
                     //* A custom native callback is generated to wrap the Kotlin callback.
                     out = [](
                         {%- for callbackArg in member.type.arguments %}
-                            {{ as_annotated_cType(callbackArg) }}{{ ',' if not loop.last }}
-                        {%- endfor %}) {
-                        UserData* userData1 = static_cast<UserData *>(userdata);
+                            {{- as_annotated_cType(callbackArg) }}{{ ', ' if not loop.last }}
+                        {%- endfor -%}
+                        {%- if member.type.category == 'function pointer' -%}
+                            //* We rely on the function pointer definitions (dawn.json) always
+                            //* including a parameter named 'userdata' as the final parameter.
+                            {%- set userdata = 'userdata' -%}
+                        {%- else %}
+                            //* Callback functions do not specify user data params in dawn.json.
+                            //* However, the C API always supplements two parameters with the names
+                            //* below.
+                            , void* userdata1, void* userdata2
+                            {%- set userdata = 'userdata1' -%}
+                        {%- endif %}) {
+                        //* User data is used to carry the JNI context (env) for use by the
+                        //* callback.
+                        UserData* userData1 = static_cast<UserData *>({{ userdata }});
                         JNIEnv *env = userData1->env;
                         if (env->ExceptionCheck()) {
                             return;
                         }
 
-                        {%- for callbackArg in kotlin_record_members(member.type.arguments) -%}
+                        {% for callbackArg in kotlin_record_members(member.type.arguments) -%}
                             {{ convert_to_kotlin(callbackArg.name.camelCase(),
                                                  '_' + callbackArg.name.camelCase(),
                                                  'input->' + callbackArg.length.name.camelCase() if callbackArg.length.name,
@@ -155,11 +168,11 @@
                         //* Call the callback with all converted parameters.
                         env->CallVoidMethod(userData1->callback, callbackMethod
                         {%- for callbackArg in kotlin_record_members(member.type.arguments) %}
-                             ,_{{ callbackArg.name.camelCase() }}
+                             {{- ', ' }}_{{ callbackArg.name.camelCase() }}
                         {%- endfor %});
                     };
                     //* TODO(b/330293719): free associated resources.
-                    outStruct->userdata = new UserData(
+                    outStruct->{{ userdata }} = new UserData(
                             {.env = env, .callback = env->NewGlobalRef(in)});
 
                 {% else %}
diff --git a/generator/templates/art/methods.cpp b/generator/templates/art/methods.cpp
index be896c9..1029be9 100644
--- a/generator/templates/art/methods.cpp
+++ b/generator/templates/art/methods.cpp
@@ -40,11 +40,6 @@
 
 namespace dawn::kotlin_api {
 
-struct UserData {
-    JNIEnv *env;
-    jobject callback;
-};
-
 jobject toByteBuffer(JNIEnv *env, const void* address, jlong size) {
     if (!address) {
         return nullptr;
diff --git a/generator/templates/art/structures.cpp b/generator/templates/art/structures.cpp
index 3aa57b9..fc910e6 100644
--- a/generator/templates/art/structures.cpp
+++ b/generator/templates/art/structures.cpp
@@ -35,6 +35,7 @@
 #include <webgpu/webgpu.h>
 
 #include "dawn/common/Assert.h"
+#include "dawn/common/Log.h"
 #include "JNIContext.h"
 
 // Converts Kotlin objects representing Dawn structures into native structures that can be passed
@@ -72,24 +73,6 @@
     *result = reinterpret_cast<T*>(env->CallObjectMethod(obj, getter));
 }
 
-// Special-case noop handling of the two callback info that are part of other structures.
-// TODO(352710628) support converting callback info.
-void ToNative(JNIContext* c, JNIEnv* env, jobject obj, WGPUDeviceLostCallbackInfo* info) {
-    *info = {};
-}
-
-void ToNative(JNIContext* c, JNIEnv* env, jobject obj, WGPUUncapturedErrorCallbackInfo* info) {
-    *info = {};
-}
-
-jobject ToKotlin(JNIEnv *env, const WGPUDeviceLostCallbackInfo* input) {
-    return nullptr;
-}
-
-jobject ToKotlin(JNIEnv *env, const WGPUUncapturedErrorCallbackInfo* input) {
-    return nullptr;
-}
-
 // Special-case [Nullable]StringView
 void ToNative(JNIContext* c, JNIEnv* env, jstring obj, WGPUStringView* s) {
     if (obj == nullptr) {
@@ -110,7 +93,7 @@
     return env->NewStringUTF(nullTerminated.c_str());
 }
 
-{%- for structure in by_category['structure'] if include_structure(structure) %}
+{%- for structure in by_category['structure'] + by_category['callback info'] if include_structure(structure) %}
 
     //* Native -> Kotlin converter.
     //* TODO(b/354411474): Filter the structures for which to add a ToKotlin conversion.
diff --git a/generator/templates/art/structures.h b/generator/templates/art/structures.h
index 89bc8c1..4f7efb4 100644
--- a/generator/templates/art/structures.h
+++ b/generator/templates/art/structures.h
@@ -31,11 +31,16 @@
 
 class JNIContext;
 
+struct UserData {
+    JNIEnv *env;
+    jobject callback;
+};
+
 // Converts Kotlin objects representing Dawn structures into native structures that can be passed
 // into the native Dawn API.
 jobject ToKotlin(JNIEnv* env, const WGPUStringView* s);
 
-{% for structure in by_category['structure'] if include_structure(structure) %}
+{% for structure in by_category['structure']  + by_category['callback info'] if include_structure(structure) %}
     jobject ToKotlin(JNIEnv *env, const {{ as_cType(structure.name) }}* input);
     void ToNative(JNIContext* c, JNIEnv* env, jobject obj, {{ as_cType(structure.name) }}* converted);
 {% endfor %}
diff --git a/tools/android/BUILD.gn b/tools/android/BUILD.gn
index 44f8e54..b773570 100644
--- a/tools/android/BUILD.gn
+++ b/tools/android/BUILD.gn
@@ -64,7 +64,9 @@
     "java/android/dawn/BufferDescriptor.kt",
     "java/android/dawn/BufferMapAsyncStatus.kt",
     "java/android/dawn/BufferMapCallback.kt",
+    "java/android/dawn/BufferMapCallback2.kt",
     "java/android/dawn/BufferMapCallbackInfo.kt",
+    "java/android/dawn/BufferMapCallbackInfo2.kt",
     "java/android/dawn/BufferMapState.kt",
     "java/android/dawn/BufferUsage.kt",
     "java/android/dawn/CallbackMode.kt",
@@ -78,7 +80,9 @@
     "java/android/dawn/CompareFunction.kt",
     "java/android/dawn/CompilationInfo.kt",
     "java/android/dawn/CompilationInfoCallback.kt",
+    "java/android/dawn/CompilationInfoCallback2.kt",
     "java/android/dawn/CompilationInfoCallbackInfo.kt",
+    "java/android/dawn/CompilationInfoCallbackInfo2.kt",
     "java/android/dawn/CompilationInfoRequestStatus.kt",
     "java/android/dawn/CompilationMessage.kt",
     "java/android/dawn/CompilationMessageType.kt",
@@ -92,16 +96,22 @@
     "java/android/dawn/ConstantEntry.kt",
     "java/android/dawn/Constants.kt",
     "java/android/dawn/CreateComputePipelineAsyncCallback.kt",
+    "java/android/dawn/CreateComputePipelineAsyncCallback2.kt",
     "java/android/dawn/CreateComputePipelineAsyncCallbackInfo.kt",
+    "java/android/dawn/CreateComputePipelineAsyncCallbackInfo2.kt",
     "java/android/dawn/CreatePipelineAsyncStatus.kt",
     "java/android/dawn/CreateRenderPipelineAsyncCallback.kt",
+    "java/android/dawn/CreateRenderPipelineAsyncCallback2.kt",
     "java/android/dawn/CreateRenderPipelineAsyncCallbackInfo.kt",
+    "java/android/dawn/CreateRenderPipelineAsyncCallbackInfo2.kt",
     "java/android/dawn/CullMode.kt",
     "java/android/dawn/DepthStencilState.kt",
     "java/android/dawn/Device.kt",
     "java/android/dawn/DeviceDescriptor.kt",
     "java/android/dawn/DeviceLostCallback.kt",
+    "java/android/dawn/DeviceLostCallback2.kt",
     "java/android/dawn/DeviceLostCallbackInfo.kt",
+    "java/android/dawn/DeviceLostCallbackInfo2.kt",
     "java/android/dawn/DeviceLostCallbackNew.kt",
     "java/android/dawn/DeviceLostReason.kt",
     "java/android/dawn/ErrorCallback.kt",
@@ -132,7 +142,9 @@
     "java/android/dawn/PipelineLayout.kt",
     "java/android/dawn/PipelineLayoutDescriptor.kt",
     "java/android/dawn/PopErrorScopeCallback.kt",
+    "java/android/dawn/PopErrorScopeCallback2.kt",
     "java/android/dawn/PopErrorScopeCallbackInfo.kt",
+    "java/android/dawn/PopErrorScopeCallbackInfo2.kt",
     "java/android/dawn/PopErrorScopeStatus.kt",
     "java/android/dawn/PowerPreference.kt",
     "java/android/dawn/PresentMode.kt",
@@ -145,7 +157,9 @@
     "java/android/dawn/Queue.kt",
     "java/android/dawn/QueueDescriptor.kt",
     "java/android/dawn/QueueWorkDoneCallback.kt",
+    "java/android/dawn/QueueWorkDoneCallback2.kt",
     "java/android/dawn/QueueWorkDoneCallbackInfo.kt",
+    "java/android/dawn/QueueWorkDoneCallbackInfo2.kt",
     "java/android/dawn/QueueWorkDoneStatus.kt",
     "java/android/dawn/RenderBundle.kt",
     "java/android/dawn/RenderBundleDescriptor.kt",
@@ -154,17 +168,21 @@
     "java/android/dawn/RenderPassColorAttachment.kt",
     "java/android/dawn/RenderPassDepthStencilAttachment.kt",
     "java/android/dawn/RenderPassDescriptor.kt",
-    "java/android/dawn/RenderPassMaxDrawCount.kt",
     "java/android/dawn/RenderPassEncoder.kt",
+    "java/android/dawn/RenderPassMaxDrawCount.kt",
     "java/android/dawn/RenderPassTimestampWrites.kt",
     "java/android/dawn/RenderPipeline.kt",
     "java/android/dawn/RenderPipelineDescriptor.kt",
     "java/android/dawn/RequestAdapterCallback.kt",
+    "java/android/dawn/RequestAdapterCallback2.kt",
     "java/android/dawn/RequestAdapterCallbackInfo.kt",
+    "java/android/dawn/RequestAdapterCallbackInfo2.kt",
     "java/android/dawn/RequestAdapterOptions.kt",
     "java/android/dawn/RequestAdapterStatus.kt",
     "java/android/dawn/RequestDeviceCallback.kt",
+    "java/android/dawn/RequestDeviceCallback2.kt",
     "java/android/dawn/RequestDeviceCallbackInfo.kt",
+    "java/android/dawn/RequestDeviceCallbackInfo2.kt",
     "java/android/dawn/RequestDeviceStatus.kt",
     "java/android/dawn/RequiredLimits.kt",
     "java/android/dawn/SType.kt",
@@ -205,7 +223,9 @@
     "java/android/dawn/TextureView.kt",
     "java/android/dawn/TextureViewDescriptor.kt",
     "java/android/dawn/TextureViewDimension.kt",
+    "java/android/dawn/UncapturedErrorCallback.kt",
     "java/android/dawn/UncapturedErrorCallbackInfo.kt",
+    "java/android/dawn/UncapturedErrorCallbackInfo2.kt",
     "java/android/dawn/VertexAttribute.kt",
     "java/android/dawn/VertexBufferLayout.kt",
     "java/android/dawn/VertexFormat.kt",
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 f91cd40..00d6265 100644
--- a/tools/android/webgpu/src/androidTest/java/android/dawn/DawnTestLauncher.kt
+++ b/tools/android/webgpu/src/androidTest/java/android/dawn/DawnTestLauncher.kt
@@ -1,10 +1,17 @@
 package android.dawn
 
-import android.dawn.helper.*
+import android.dawn.CallbackMode.Companion.WaitAnyOnly
+import android.dawn.helper.DawnException
+import android.dawn.helper.Util
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 
+class DeviceLostException(val device: Device, val reason: DeviceLostReason, message: String) :
+    Exception(message)
+
+class UncapturedErrorException(val device: Device, val type: ErrorType, message: String) : Exception(message)
+
 fun dawnTestLauncher(
     requiredFeatures: Array<FeatureName> = arrayOf(),
     callback: suspend (device: Device) -> Unit
@@ -26,13 +33,20 @@
             instance.requestAdapter().adapter ?: throw DawnException("No adapter available")
 
         val device = adapter.requestDevice(
-            DeviceDescriptor(requiredFeatures = requiredFeatures)
+            DeviceDescriptor(
+                requiredFeatures = requiredFeatures,
+                deviceLostCallbackInfo2 = DeviceLostCallbackInfo2(
+                    callback = DeviceLostCallback2
+                    { device, reason, message ->
+                        throw DeviceLostException(device, reason, message)
+                    },
+                    mode = WaitAnyOnly
+                ),
+                uncapturedErrorCallbackInfo2 = UncapturedErrorCallbackInfo2 { device, type, message ->
+                    throw UncapturedErrorException(device, type, message)
+                })
         ).device ?: throw DawnException("No device available")
 
-        device.setUncapturedErrorCallback { type, message ->
-            throw DawnException(message)
-        }
-
         callback(device)
 
         device.close()
@@ -43,6 +57,18 @@
         runBlocking {
             eventProcessor.join()
         }
-        instance.close()
+        var caughtDeviceLostException = false;
+        try {
+            instance.close()
+        } catch (ignored: DeviceLostException) {
+            // For some reason we receive a device lost callback even though the only device has
+            // been closed. b/381416258
+            caughtDeviceLostException = true;
+        }
+        assert(caughtDeviceLostException) {
+            "When this assert stops passing, it indicates that the DeviceLostException we're " +
+                    "catching above is no longer being erroneously thrown, so we can safely " +
+                    "remove the try/catch and treat the DLE as a genuine error."
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/tools/android/webgpu/src/androidTest/java/android/dawn/ErrorTest.kt b/tools/android/webgpu/src/androidTest/java/android/dawn/ErrorTest.kt
new file mode 100644
index 0000000..4bbf137
--- /dev/null
+++ b/tools/android/webgpu/src/androidTest/java/android/dawn/ErrorTest.kt
@@ -0,0 +1,28 @@
+package android.dawn
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ErrorTest {
+    @Test
+    /**
+     * Test that an invalid parameter raises an error that is converted to a Kotlin exception by
+     * the adapter in DawnTestLauncher.
+     */
+    fun errorTest() {
+        dawnTestLauncher { device ->
+            assertThrows(UncapturedErrorException::class.java) {
+                device.createTexture(
+                    TextureDescriptor(
+                        usage = TextureUsage.None,
+                        size = Extent3D(0),
+                        format = TextureFormat.Undefined  // Invalid parameter for createTexture().
+                    )
+                )
+            }
+        }
+    }
+}