[Kotlin]: Add a KotlinRecord concept, factor struct conversion with it

In the Dawn code generators, both structures and methods are considered
"records" that have a bunch of named parameter/members with types. The
conversion of structures and method records from Kotlin to C++ are very
similar. To merge them both, a KotlinRecord concept is added that is a
C++ structure for a the JNI objects representing the values of the
Kotlin records.

A macro is added that creates the KotlinRecord structure for a record
(either a structure, or a method).

A macro is added that creates a conversion function from a KotlinRecord
to a structure.

These two macros are used to factor out all of the common logic out of
structures.cpp. The logic in methods.cpp will be factored out in a
follow-up CL.

A few new primitive types are added to dawn_kotlin.json.

Helper CallGetter functions are added in structures.cpp to call the
correct JNIEnv::Call*Method depending on what return type we expect.

A couple hacks for handling the jlong ANativeWindow are added. We should
remove them with a manually written method.

Bug: 352711433
Change-Id: Iaa89c9e7e85f724aa3fd98136acbad176f68e522
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/198236
Reviewed-by: Sonakshi Saxena <nexa@google.com>
Reviewed-by: Jim Blackler <jimblackler@google.com>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index 856bef9..4a1d466 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -853,7 +853,7 @@
                         and argument.type.category == 'structure'):
                     return argument
 
-        return {"type": method.return_type}
+        return {"type": method.return_type, "name": None}
 
     # TODO(b/352047733): Replace methods that require special handling with an exceptions list.
     def include_method(method):
@@ -1663,6 +1663,7 @@
 
             imported_templates += [
                 "art/api_jni_types.kt",
+                "art/kotlin_record_conversion.cpp",
             ]
 
             renders.append(
diff --git a/generator/templates/art/kotlin_record_conversion.cpp b/generator/templates/art/kotlin_record_conversion.cpp
new file mode 100644
index 0000000..e118ce6
--- /dev/null
+++ b/generator/templates/art/kotlin_record_conversion.cpp
@@ -0,0 +1,142 @@
+//* 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.
+{% from 'art/api_jni_types.kt' import arg_to_jni_type with context %}
+
+{% macro define_kotlin_record_structure(struct_name, members) %}
+    struct {{struct_name}} {
+        {% for member in kotlin_record_members(members) %}
+            //* HACK: Hardcode that ANativeWindow is a jlong instead of an actual pointer. Instead
+            //* of this, we should have manually written method that directly creates the
+            //* wgpu::Surface from the Java Surface.
+            {% if member.name.get() == "window" and member.type.name.get() == "void *"%}
+                jlong {{ as_varName(member.name) }};
+            {% else %}
+                {{ arg_to_jni_type(member) }} {{ as_varName(member.name) }};
+            {% endif %}
+        {% endfor%}
+    };
+{% endmacro %}
+
+{% macro define_kotlin_to_struct_conversion(function_name, kotlin_name, struct_name, members) %}
+    inline void {{function_name}}(JNIContext* c, const {{kotlin_name}}& inStruct, {{struct_name}}* outStruct) {
+        JNIEnv* env = c->env;
+        *outStruct = {};
+        {% for member in kotlin_record_members(members) %} {
+            auto& in = inStruct.{{member.name.camelCase()}};
+            auto& out = outStruct->{{member.name.camelCase()}};
+            {% if member.length == 'strlen' %}
+                if (in != nullptr) {
+                    out = c->GetStringUTFChars(in);
+                } else {
+                    out = nullptr;
+                }
+
+            {% elif member.constant_length == 1 %}
+                {% if member.type.category == 'structure' %}
+                    if (in != nullptr) {
+                        auto converted = c->Alloc<{{as_cType(member.type.name)}}>();
+                        ToNative(c, env, in, converted);
+                        out = converted;
+                    }
+                {% elif member.type.name.get() == 'void' %}
+                    out = reinterpret_cast<void*>(in);
+                {% else %}
+                    {{ unreachable_code() }}
+                {% endif %}
+
+            {% elif member.length %}
+                auto& outLength = outStruct->{{member.length.name.camelCase()}};
+
+                {% if member.constant_length %}
+                    {{ unreachable_code() }}
+                {% endif %}
+
+                //* Convert container, including the length field.
+                {% if member.type.name.get() == 'uint32_t' %}
+                    //* This container type is represented in Kotlin as a primitive array.
+                    out = reinterpret_cast<const uint32_t*>(c->GetIntArrayElements(in));
+                    outLength = env->GetArrayLength(in);
+
+                {% else %}
+                    //* These container types are represented in Kotlin as arrays of objects.
+                    outLength = env->GetArrayLength(in);
+                    auto array = c->AllocArray<{{ as_cType(member.type.name) }}>(outLength);
+                    out = array;
+
+                    {% if member.type.category in ['bitmask', 'enum'] %}
+                        jclass memberClass = env->FindClass("{{ jni_name(member.type) }}");
+                        jmethodID getValue = env->GetMethodID(memberClass, "getValue", "()I");
+                        for (int idx = 0; idx != outLength; idx++) {
+                            jobject element = env->GetObjectArrayElement(in, idx);
+                            array[idx] = static_cast<{{ as_cType(member.type.name) }}>(
+                                    env->CallIntMethod(element, getValue));
+                        }
+                    {% elif member.type.category == 'object' %}
+                        jclass memberClass = env->FindClass("{{ jni_name(member.type) }}");
+                        jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
+                        for (int idx = 0; idx != outLength; idx++) {
+                            jobject element = env->GetObjectArrayElement(in, idx);
+                            array[idx] = reinterpret_cast<{{ as_cType(member.type.name) }}>(
+                                    env->CallLongMethod(element, getHandle));
+                        }
+                    {% elif member.type.category == 'structure' %}
+                        for (int idx = 0; idx != outLength; idx++) {
+                            ToNative(c, env, env->GetObjectArrayElement(in, idx), &array[idx]);
+                        }
+                    {% else %}
+                        {{ unreachable_code() }}
+                    {% endif %}
+                {% endif %}
+
+            //* From here members are single values.
+            {% elif member.type.category == 'object' %}
+                if (in != nullptr) {
+                    jclass memberClass = env->FindClass("{{ jni_name(member.type) }}");
+                    jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
+                    out = reinterpret_cast<{{as_cType(member.type.name)}}>(
+                            env->CallLongMethod(in, getHandle));
+                } else {
+                    out = nullptr;
+                }
+            {% elif member.type.category == 'structure' %}
+                //* Mandatory structure.
+                ToNative(c, env, in, &out);
+
+            {% elif member.name.get() == "window" and member.type.name.get() == "void *" %}
+                //* HACK: Hardcode that ANativeWindow is a jlong instead of an actual pointer. Instead
+                //* of this, we should have manually written method that directly creates the
+                //* wgpu::Surface from the Java Surface.
+                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);
+            {% else %}
+                {{ unreachable_code() }}
+            {% endif %}
+        } {% endfor %}
+    }
+{% endmacro %}
diff --git a/generator/templates/art/structures.cpp b/generator/templates/art/structures.cpp
index e2b565a..f40f7d6 100644
--- a/generator/templates/art/structures.cpp
+++ b/generator/templates/art/structures.cpp
@@ -25,7 +25,7 @@
 //* 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.
 {% from 'art/api_jni_types.kt' import convert_to_kotlin, jni_signature with context %}
-
+{% from 'art/kotlin_record_conversion.cpp' import define_kotlin_record_structure, define_kotlin_to_struct_conversion with context %}
 #include "structures.h"
 
 #include <jni.h>
@@ -39,6 +39,36 @@
 
 namespace dawn::kotlin_api {
 
+// Helper functions to call the correct JNIEnv::Call*Method depending on what return type we expect.
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jboolean* result) {
+    *result = env->CallBooleanMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jbyte* result) {
+    *result = env->CallByteMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jchar* result) {
+    *result = env->CallCharMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jshort* result) {
+    *result = env->CallShortMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jint* result) {
+    *result = env->CallIntMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jlong* result) {
+    *result = env->CallLongMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jfloat* result) {
+    *result = env->CallFloatMethod(obj, getter);
+}
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, jdouble* result) {
+    *result = env->CallDoubleMethod(obj, getter);
+}
+template <typename T>
+void CallGetter(JNIEnv* env, jmethodID getter, jobject obj, T** result) {
+    *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) {
@@ -114,129 +144,26 @@
         return converted;
     }
 
+    {% set Struct = as_cType(structure.name) %}
+    {% set KotlinRecord = "KotlinRecord" + structure.name.CamelCase() %}
+    {{ define_kotlin_record_structure(KotlinRecord, structure.members)}}
+    {{ define_kotlin_to_struct_conversion("ConvertInternal", KotlinRecord, Struct, structure.members)}}
+
     void ToNative(JNIContext* c, JNIEnv* env, jobject obj, {{ as_cType(structure.name) }}* converted) {
         jclass clz = env->FindClass("{{ jni_name(structure) }}");
 
-        //* Convert each member in turn from corresponding members of the Kotlin object obtained via
-        //* JNI calls
+        //* Use getters to fill in the Kotlin record that will get converted to our struct.
+        {{KotlinRecord}} kotlinRecord;
         {% for member in kotlin_record_members(structure.members) %} {
-            jmethodID method = env->GetMethodID(
-                    clz, "get{{ member.name.CamelCase() }}", "(){{ jni_signature(member) }}");
-            {% if member.length == 'strlen' %}
-                jobject mObj = env->CallObjectMethod(obj, method);
-                if (mObj) {
-                    converted->{{ member.name.camelCase() }} =
-                            c->GetStringUTFChars(reinterpret_cast<jstring>(mObj));
-                } else {
-                    converted->{{ member.name.camelCase() }} = nullptr;
-                }
-            {% elif member.constant_length == 1 %}
-                {% if member.type.category == 'structure' %}
-                    //* Convert optional structure if present.
-                    jobject mObj = env->CallObjectMethod(obj, method);
-                    if (mObj) {
-                        auto convertedMember = c->Alloc<{{ as_cType(member.type.name) }}>();
-                        ToNative(c, env, mObj, convertedMember);
-                        converted->{{ member.name.camelCase() }} = convertedMember;
-                    }
-                {% elif member.type.name.get() == 'void' %}
-                    converted->{{ member.name.camelCase() }} =
-                            reinterpret_cast<void*>(env->CallLongMethod(obj, method));
-                {% else %}
-                    {{ unreachable_code() }}
-                {% endif %}
-
-            {% elif member.length %}
-                {% if member.constant_length %}
-                    {{ unreachable_code() }}
-                {% endif %}
-                //* Convert container, including the length field.
-                {% if member.type.name.get() == 'uint32_t' %} {
-                    //* This container type is represented in Kotlin as a primitive array.
-                    jintArray array = static_cast<jintArray>(env->CallObjectMethod(obj, method));
-                    converted->{{ member.name.camelCase() }} =
-                            reinterpret_cast<const {{ as_cType(member.type.name) }}*>(
-                                   c->GetIntArrayElements(array));
-                    converted->{{ member.length.name.camelCase() }} = env->GetArrayLength(array);
-                }
-                {% else %} {
-                    //* These container types are represented in Kotlin as arrays of objects.
-                    auto in = static_cast<jobjectArray>(env->CallObjectMethod(obj, method));
-                    size_t length = env->GetArrayLength(in);
-                    auto out = c->AllocArray<{{ as_cType(member.type.name) }}>(length);
-                    {% if member.type.category in ['bitmask', 'enum'] %} {
-                        jclass memberClass = env->FindClass("{{ jni_name(member.type) }}");
-                        jmethodID getValue = env->GetMethodID(memberClass, "getValue", "()I");
-                        for (int idx = 0; idx != length; idx++) {
-                            jobject element = env->GetObjectArrayElement(in, idx);
-                            out[idx] = static_cast<{{ as_cType(member.type.name) }}>(
-                                    env->CallIntMethod(element, getValue));
-                        }
-                    }
-                    {% elif member.type.category == 'object' %} {
-                        jclass memberClass = env->FindClass("{{ jni_name(member.type) }}");
-                        jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
-                        for (int idx = 0; idx != length; idx++) {
-                            jobject element = env->GetObjectArrayElement(in, idx);
-                            out[idx] = reinterpret_cast<{{ as_cType(member.type.name) }}>(
-                                    env->CallLongMethod(element, getHandle));
-                        }
-                    }
-                    {% elif member.type.category == 'structure' %}
-                        for (int idx = 0; idx != length; idx++) {
-                            ToNative(c, env, env->GetObjectArrayElement(in, idx), out + idx);
-                        }
-                    {% else %}
-                        {{ unreachable_code() }}
-                    {% endif %}
-                    converted->{{ member.name.camelCase() }} = out;
-                    converted->{{ member.length.name.camelCase() }} = length;
-                }
-                {% endif %}
-
-            //* From here members are single values.
-            {% elif member.type.category == 'object' %}
-                jobject mObj = env->CallObjectMethod(obj, method);
-                if (mObj) {
-                    jclass memberClass = env->FindClass("{{ jni_name(member.type) }}");
-                    jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
-                    converted->{{ member.name.camelCase() }} =
-                            reinterpret_cast<{{ as_cType(member.type.name) }}>(
-                                    env->CallLongMethod(mObj, getHandle));
-                } else {
-                    converted->{{ member.name.camelCase() }} = nullptr;
-                }
-            {% else %}
-                {% if member.type.category == 'structure' or member.type.category == 'callback info' %}
-                    //* Mandatory structure.
-                    ToNative(c, env, env->CallObjectMethod(obj, method),
-                            &converted->{{ member.name.camelCase() }});
-                {% elif member.type.name.get() == 'void *' %}
-                    converted->{{ member.name.camelCase() }} =
-                        reinterpret_cast<void*>(static_cast<intptr_t>(env->CallLongMethod(obj, method)));
-                {% else %}
-                    converted->{{ member.name.camelCase() }} =
-                            static_cast<{{ as_cType(member.type.name) }}>(
-                    {% if member.type.name.get() == 'bool' %}
-                        env->CallBooleanMethod
-                    {% elif member.type.name.get() == 'uint16_t' %}
-                        env->CallShortMethod
-                    {% elif member.type.name.get() in ['int', 'int32_t', 'uint32_t']
-                            or member.type.category in ['bitmask', 'enum'] %}
-                        env->CallIntMethod
-                    {% elif member.type.name.get() == 'float' %}
-                        env->CallFloatMethod
-                    {% elif member.type.name.get() in ['size_t', 'uint64_t'] %}
-                        env->CallLongMethod
-                    {% elif member.type.name.get() == 'double' %}
-                        env->CallDoubleMethod
-                    {% else %}
-                        {{ unreachable_code() }}
-                    {% endif %} (obj, method));
-                {% endif %}
-            {% endif %}
+            jmethodID getter = env->GetMethodID(clz,
+                                                "get{{member.name.CamelCase()}}",
+                                                "(){{jni_signature(member)}}");
+            CallGetter(env, getter, obj, &kotlinRecord.{{as_varName(member.name)}});
         } {% endfor %}
 
+        //* Fill all struct members from the Kotlin record.
+        ConvertInternal(c, kotlinRecord, converted);
+
         //* Set up the chain type and links for child objects.
         {% if structure.chained %}
             converted->chain = {.sType = WGPUSType_{{ structure.name.CamelCase() }}};
@@ -252,8 +179,7 @@
                 out->chain.next = converted->nextInChain;
                 converted->nextInChain = &out->chain;
             }
-        }
-        {% endfor %}
+        }{% endfor %}
     }
 {% endfor %}
 
diff --git a/src/dawn/dawn_kotlin.json b/src/dawn/dawn_kotlin.json
index 3db8ed3..0860452 100644
--- a/src/dawn/dawn_kotlin.json
+++ b/src/dawn/dawn_kotlin.json
@@ -36,8 +36,9 @@
         "bool": "jboolean",
         "double": "jdouble",
         "float": "jfloat",
-        "int32_t": "jint",
+        "double": "jdouble",
         "size_t": "jlong",
+        "int32_t": "jint",
         "uint16_t": "jshort",
         "uint32_t": "jint",
         "uint64_t": "jlong",