Add initial version of the Kotlin JNI conversion

Bug: 307286150

Change-Id: Ifffca0b584b64cd428ba372194d45700c6c22cb3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/190623
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Jim Blackler <jimblackler@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index 484d01d..90bf762 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -795,6 +795,7 @@
 def compute_kotlin_params(loaded_json, kotlin_json):
     params_kotlin = parse_json(loaded_json, enabled_tags=['art'])
     params_kotlin['kotlin_package'] = kotlin_json['kotlin_package']
+    params_kotlin['to_jni_type'] = kotlin_json['to_jni_type']
     kt_file_path = params_kotlin['kotlin_package'].replace('.', '/')
 
     # The 'length' members are removed as Kotlin can infer that from the container.
@@ -1502,6 +1503,15 @@
                 FileRender('art/api_kotlin_functions.kt',
                            'java/' + kt_file_path + '/Functions.kt',
                            [RENDER_PARAMS_BASE, params_kotlin]))
+            renders.append(
+                FileRender('art/structures.h', 'cpp/structures.h',
+                           [RENDER_PARAMS_BASE, params_kotlin]))
+            renders.append(
+                FileRender('art/structures.cpp', 'cpp/structures.cpp',
+                           [RENDER_PARAMS_BASE, params_kotlin]))
+            renders.append(
+                FileRender('art/methods.cpp', 'cpp/methods.cpp',
+                           [RENDER_PARAMS_BASE, params_kotlin]))
 
             for enum in (params_kotlin['by_category']['bitmask'] +
                          params_kotlin['by_category']['enum']):
diff --git a/generator/templates/art/methods.cpp b/generator/templates/art/methods.cpp
new file mode 100644
index 0000000..09dd9ed
--- /dev/null
+++ b/generator/templates/art/methods.cpp
@@ -0,0 +1,291 @@
+//* 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.
+#include <jni.h>
+#include "dawn/webgpu.h"
+#include "structures.h"
+
+// Converts every method call from the Kotlin-hosted version to the native version, performing
+// the necessary JNI calls to fetch data, and converting the data to the correct form.
+
+#define DEFAULT __attribute__((visibility("default")))
+
+namespace dawn::kotlin_api {
+
+struct UserData {
+    JNIEnv *env;
+    jobject callback;
+};
+
+{% macro render_method(method, object) %}
+    //*  A JNI-external method is built with the JNI signature expected to match the host Kotlin.
+    DEFAULT extern "C"
+    {% if method.return_type.category == 'structure' %}
+        {{ unreachable_code() }}   //*  Currently returning structures is not supported.
+    {% elif method.return_type.category in ['bitmask', 'enum'] -%}
+        jint
+    {%- elif method.return_type.category == 'object' -%}
+        jobject
+    {%- else -%}
+        {{ to_jni_type[method.return_type.name.get()] }}
+    {%- endif %}
+    {{ ' ' }}
+    Java_{{ kotlin_package.replace('.', '_') }}_
+            {{- object.name.CamelCase() if object else 'FunctionsKt' -}}
+            _{{ method.name.camelCase() }}(JNIEnv *env
+                    {{ ', jobject obj' if object else ', jclass clazz' -}}
+
+    {% set input_args = [] %}
+    //* Filter the list of method arguments. We ignore userdata and length arguments which are not
+    //* present in the Kotlin host method.
+    {% for arg in method.arguments if arg.name.get() != 'userdata'
+            and not method.arguments | selectattr('length', 'equalto', arg) | first %}
+        {% do input_args.append(arg) %}
+    {% endfor %}
+
+    //* Make the signature for each argument in turn.
+    {% for arg in input_args %},
+        {%- if arg.length == 'strlen' -%}
+            jstring
+        {%- elif arg.length and arg.constant_length != 1 %}
+            {%- if arg.type.category in [
+                    'bitmask', 'enum', 'function pointer', 'object', 'structure'] -%}
+                jobjectArray
+            {%- elif arg.type.name.get() == 'void' -%}
+                jobject
+            {%- elif arg.type.name.get() == 'uint32_t' -%}
+                jintArray
+            {%- else %}
+                //* Currently returning structures in callbacks is not supported.
+                {{ unreachable_code() }}
+            {% endif %}
+        {%- elif arg.type.category in ['function pointer', 'object', 'structure'] -%}
+            jobject
+        {%- elif arg.type.category in ['bitmask', 'enum'] -%}
+            jint
+        {%- elif arg.type.name.get() in ['float',  'int32_t', 'uint32_t', 'uint64_t', 'size_t'] -%}
+            {{ to_jni_type[arg.type.name.get()] }}
+        {%- else %}
+            {{ unreachable_code() }}
+        {% endif %}
+        {{ ' ' }}_{{ as_varName(arg.name) }}
+    {% endfor %}) {
+
+    //*  A variable is declared for each parameter of the native method.
+    {% for arg in method.arguments %}
+        {{ as_annotated_cType(arg) }};
+    {% endfor %}
+
+    //* Each parameter is converted from the JNI parameter to the expected form of the native
+    //* parameter.
+    {% for arg in input_args %}
+        {% if arg.length == 'strlen' %}
+            if (_{{ as_varName(arg.name) }}) {  //* Don't convert null strings.
+                {{ as_varName(arg.name) }} = env->GetStringUTFChars(_{{ as_varName(arg.name) }}, 0);
+            } else {
+                {{ as_varName(arg.name) }} = nullptr;
+            }
+        {% elif arg.constant_length == 1 %}
+            //*  Optional structure.
+            {% if arg.type.category == 'structure' %}
+                if (_{{ as_varName(arg.name) }}) {
+                    //* TODO(b/330293719): free associated resources.
+                    auto convertedMember = new {{ as_cType(arg.type.name) }}();
+                    Convert(env, _{{ as_varName(arg.name) }}, convertedMember);
+                    {{ as_varName(arg.name) }} = convertedMember;
+                } else {
+                    {{ as_varName(arg.name) }} = nullptr;
+                }
+            {% else %}
+                {{ unreachable_code() }}
+            {% endif %}
+        {% elif arg.length %}
+            //*  Container types.
+            {% if arg.type.name.get() == 'uint32_t' %}
+                //* TODO(b/330293719): free associated resources.
+                {{ as_varName(arg.name) }} =
+                        reinterpret_cast<{{ as_cType(arg.type.name) }}*>(
+                               env->GetIntArrayElements(_{{ as_varName(arg.name) }}, 0));
+               {{ arg.length.name.camelCase() }} =
+                       env->GetArrayLength(_{{ as_varName(arg.name) }});
+            {% elif arg.type.name.get() == 'void' %}
+                {{ as_varName(arg.name) }} =
+                        env->GetDirectBufferAddress(_{{ as_varName(arg.name) }});
+                {{ arg.length.name.camelCase() }} =
+                        env->GetDirectBufferCapacity(_{{ as_varName(arg.name) }});
+            {% else %} {
+                size_t length = env->GetArrayLength(_{{ as_varName(arg.name) }});
+                //* TODO(b/330293719): free associated resources.
+                auto out = new {{ as_cType(arg.type.name) }}[length]();
+                {% if arg.type.category in ['bitmask', 'enum'] %} {
+                    jclass memberClass = env->FindClass("{{ jni_name(arg.type) }}");
+                    jmethodID getValue = env->GetMethodID(memberClass, "getValue", "()I");
+                    for (int idx = 0; idx != length; idx++) {
+                        jobject element =
+                                env->GetObjectArrayElement(_{{ as_varName(arg.name) }}, idx);
+                        out[idx] = static_cast<{{ as_cType(arg.type.name) }}>(
+                                env->CallIntMethod(element, getValue));
+                    }
+                }
+                {% elif arg.type.category == 'object' %} {
+                    jclass memberClass = env->FindClass("{{ jni_name(arg.type) }}");
+                    jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
+                    for (int idx = 0; idx != length; idx++) {
+                        jobject element =
+                                env->GetObjectArrayElement(_{{ as_varName(arg.name) }}, idx);
+                        out[idx] = reinterpret_cast<{{ as_cType(arg.type.name) }}>(
+                                env->CallLongMethod(element, getHandle));
+                    }
+                }
+                {% else %}
+                    {{ unreachable_code() }}
+                {% endif %}
+                {{ as_varName(arg.name) }} = out;
+                {{ arg.length.name.camelCase() }} = length;
+            }
+            {% endif %}
+
+        //*  Single value types.
+        {% elif arg.type.category == 'object' %}
+            if (_{{ as_varName(arg.name) }}) {
+                jclass memberClass = env->FindClass("{{ jni_name(arg.type) }}");
+                jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
+                {{ as_varName(arg.name) }} =
+                        reinterpret_cast<{{ as_cType(arg.type.name) }}>(
+                                env->CallLongMethod(_{{ as_varName(arg.name) }}, getHandle));
+            } else {
+                {{ as_varName(arg.name) }} = nullptr;
+            }
+        {% elif arg.type.name.get() in ['int32_t', 'size_t', 'uint32_t', 'uint64_t']
+                or arg.type.category in ['bitmask', 'enum'] %}
+            {{ as_varName(arg.name) }} =
+                    static_cast<{{ as_cType(arg.type.name) }}>(_{{ as_varName(arg.name) }});
+        {% elif arg.type.name.get() in ['float', 'int'] %}
+            {{ as_varName(arg.name) }} = _{{ as_varName(arg.name) }};
+
+        {% elif arg.type.category == 'function pointer' %} {
+            //* Function pointers themselves require each argument converting.
+            //* A custom native callback is generated to wrap the Kotlin callback.
+            {{ as_varName(arg.name) }} = [](
+                {%- for callbackArg in arg.type.arguments %}
+                    {{ as_annotated_cType(callbackArg) }}{{ ',' if not loop.last }}
+                {%- endfor %}) {
+                UserData* userData1 = static_cast<UserData *>(userdata);
+                JNIEnv *env = userData1->env;
+                if (env->ExceptionCheck()) {
+                    return;
+                }
+
+                //* Get the client (Kotlin) callback so we can call it.
+                jmethodID callbackMethod = env->GetMethodID(
+                        env->FindClass("{{ jni_name(arg.type) }}"), "callback", "(
+                    {%- for callbackArg in arg.type.arguments if
+                            callbackArg.name.get() != 'userdata'-%}
+                        {%- if callbackArg.type.category in ['bitmask', 'enum'] -%}
+                            I
+                        {%- elif callbackArg.type.category in ['object', 'structure'] -%}
+                            L{{ jni_name(callbackArg.type) }};
+                        {%- elif callbackArg.type.name.get() == 'char' -%}
+                            Ljava/lang/String;
+                        {%- else -%}
+                            {{ unreachable_code() }}
+                        {%- endif -%}
+                    {%- endfor %})V");
+
+                //* Call the callback with all converted parameters.
+                env->CallVoidMethod(userData1->callback, callbackMethod
+                {%- for callbackArg in arg.type.arguments if callbackArg.name.get() != 'userdata' %}
+                    ,
+                    {%- if callbackArg.type.category == 'object' %}
+                        env->NewObject(env->FindClass("{{ jni_name(callbackArg.type) }}"),
+                                env->GetMethodID(env->FindClass("{{ jni_name(callbackArg.type) }}"),
+                                        "<init>", "(J)V"),
+                                reinterpret_cast<jlong>({{ callbackArg.name.camelCase() }}))
+                    {%- elif callbackArg.type.category in ['bitmask', 'enum'] %}
+                        static_cast<jint>({{ callbackArg.name.camelCase() }})
+                    {%- elif callbackArg.type.category == 'structure' %}
+                        {{ unreachable_code () }}  //* We don't yet handle structures in callbacks.
+                    {%- elif callbackArg.type.name.get() == 'char' %}
+                        env->NewStringUTF({{ callbackArg.name.camelCase() }})
+                    {%- else %}
+                        {{ callbackArg.name.camelCase() }}
+                    {%- endif %}
+                {%- endfor %});
+            };
+            //* TODO(b/330293719): free associated resources.
+            userdata = new UserData(
+                    {.env = env, .callback = env->NewGlobalRef(_{{ as_varName(arg.name) }})});
+        }
+        {% else %}
+            {{ unreachable_code() }}
+        {% endif %}
+    {% endfor %}
+
+    {% if object %}
+        jclass memberClass = env->FindClass("{{ jni_name(object) }}");
+        jmethodID getHandle = env->GetMethodID(memberClass, "getHandle", "()J");
+        auto handle =
+                reinterpret_cast<{{ as_cType(object.name) }}>(env->CallLongMethod(obj, getHandle));
+    {% endif %}
+
+    //* Actually invoke the native version of the method.
+    {{ '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' }};
+    }
+
+    //* We only handle objects and primitives to be returned.
+    {% if method.return_type.category == 'object' %}
+        jclass returnClass = env->FindClass("{{ jni_name(method.return_type) }}");
+        auto constructor = env->GetMethodID(returnClass, "<init>", "(J)V");
+        return env->NewObject(returnClass, constructor, reinterpret_cast<jlong>(result));
+    {% elif method.return_type.name.get() != 'void' %}
+        return result;  //* Primitives are implicitly converted by JNI.
+    {% endif %}
+}
+{% endmacro %}
+
+{% for obj in by_category['object'] %}
+    {% for method in obj.methods if include_method(method) %}
+        {{ render_method(method, obj) }}
+    {% endfor %}
+{% endfor %}
+
+//* Global functions don't have an associated class.
+{% for function in by_category['function'] if include_method(function) %}
+    {{ render_method(function, None) }}
+{% endfor %}
+
+}  // namespace dawn::kotlin_api
diff --git a/generator/templates/art/structures.cpp b/generator/templates/art/structures.cpp
new file mode 100644
index 0000000..af901f4
--- /dev/null
+++ b/generator/templates/art/structures.cpp
@@ -0,0 +1,193 @@
+//* 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.
+#include <jni.h>
+#include "dawn/webgpu.h"
+#include "structures.h"
+
+// Converts Kotlin objects representing Dawn structures into native structures that can be passed
+// into the native Dawn API.
+
+namespace dawn::kotlin_api {
+
+{% for structure in by_category['structure'] %}
+    void Convert(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
+        {% for member in structure.members if include_structure_member(structure, member) %}
+            {% if member.length == 'strlen' %} {
+                jobject mObj = env->CallObjectMethod(obj,
+                        env->GetMethodID(clz, "get{{ member.name.CamelCase() }}",
+                                "()Ljava/lang/String;"));
+                if (mObj) {
+                    //* TODO(b/330293719): free associated resources.
+                    converted->{{ member.name.camelCase() }} =
+                            env->GetStringUTFChars(static_cast<jstring>(mObj), 0);
+                }
+            }
+            {% elif member.constant_length == 1 %} {
+                {% if member.type.category == 'structure' %}
+                    //* Convert optional structure if present.
+                    jobject mObj = env->CallObjectMethod(obj,
+                            env->GetMethodID(clz, "get{{ member.name.CamelCase() }}",
+                                    "()L{{ jni_name(member.type) }};"));
+                    if (mObj) {
+                        //* TODO(b/330293719): free associated resources.
+                        auto convertedMember = new {{ as_cType(member.type.name) }}();
+                        Convert(env, mObj, convertedMember);
+                        converted->{{ member.name.camelCase() }} = convertedMember;
+                    }
+                {% elif member.type.name.get() == 'void' %}
+                    converted->{{ member.name.camelCase() }} = reinterpret_cast<void*>(
+                            env->CallLongMethod(obj, env->GetMethodID(
+                                    clz, "get{{ member.name.CamelCase() }}", "()J")));
+                {% 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,
+                            env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()[I")));
+                    //* TODO(b/330293719): free associated resources.
+                    converted->{{ member.name.camelCase() }} =
+                            reinterpret_cast<{{ as_cType(member.type.name) }}*>(
+                                   env->GetIntArrayElements(array, 0));
+                    converted->{{ member.length.name.camelCase() }} = env->GetArrayLength(array);
+                }
+                {% else %} {
+                    //* These container types are represented in Kotlin as arrays of objects.
+                    jmethodID getMethod = env->GetMethodID(clz,
+                            "get{{ member.name.CamelCase() }}", "()[L{{ jni_name(member.type) }};");
+                    auto in = static_cast<jobjectArray>(env->CallObjectMethod(obj, getMethod));
+                    size_t length = env->GetArrayLength(in);
+                    //* TODO(b/330293719): free associated resources.
+                    auto out = new {{ 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++) {
+                            Convert(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, env->GetMethodID(clz,
+                        "get{{ member.name.CamelCase() }}", "()L{{ jni_name(member.type) }};"));
+                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));
+                }
+            }
+            {% elif member.type.category == 'structure' %}
+                //* Mandatory structure.
+                Convert(env, env->CallObjectMethod(obj,
+                        env->GetMethodID(clz, "get{{ member.name.CamelCase() }}",
+                                "()L{{ jni_name(member.type) }};")),
+                                &converted->{{ member.name.camelCase() }});
+            {% elif member.type.name.get() == 'bool' %}
+                converted->{{ member.name.camelCase() }} = env->CallBooleanMethod(obj,
+                        env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()Z"));
+            {% elif member.type.name.get() == 'uint16_t' %}
+                converted->{{ member.name.camelCase() }} =
+                        static_cast<{{ as_cType(member.type.name) }}>(env->CallShortMethod(obj,
+                                env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()S")));
+            {% elif member.type.name.get() in ['int', 'int32_t', 'uint32_t']
+                    or member.type.category in ['bitmask', 'enum'] %}
+                converted->{{ member.name.camelCase() }} =
+                        static_cast<{{ as_cType(member.type.name) }}>(env->CallIntMethod(obj,
+                                env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()I")));
+            {% elif member.type.name.get() == 'float' %}
+                converted->{{ member.name.camelCase() }} = env->CallFloatMethod(obj,
+                        env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()F"));
+            {% elif member.type.name.get() in ['size_t', 'uint64_t'] %}
+                converted->{{ member.name.camelCase() }} =
+                        static_cast<{{ as_cType(member.type.name) }}>(env->CallLongMethod(obj,
+                                env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()J")));
+            {% elif member.type.name.get() == 'double' %}
+                converted->{{ member.name.camelCase() }} = env->CallDoubleMethod(obj,
+                        env->GetMethodID(clz, "get{{ member.name.CamelCase() }}", "()D"));
+            {% else %}
+                {{ unreachable_code() }}
+            {% endif %}
+        {% endfor %}
+
+        //* Set up the chain type and links for child objects.
+        {% if structure.chained %}
+            converted->chain = {.sType = WGPUSType_{{ structure.name.CamelCase() }}};
+        {% endif %}
+
+        {% for child in chain_children[structure.name.get()] %} {
+            jobject child = env->CallObjectMethod(obj,
+                    env->GetMethodID(clz, "get{{ child.name.CamelCase() }}",
+                            "()L{{ jni_name(child) }};"));
+            if (child) {
+                //* TODO(b/330293719): free associated resources.
+                auto out = new {{ as_cType(child.name) }}();
+                Convert(env, child, out);
+                out->chain.next = converted->nextInChain;
+                converted->nextInChain = &out->chain;
+            }
+        }
+        {% endfor %}
+    }
+{% endfor %}
+
+}  // namespace dawn::kotlin_api
diff --git a/generator/templates/art/structures.h b/generator/templates/art/structures.h
new file mode 100644
index 0000000..c4552ca
--- /dev/null
+++ b/generator/templates/art/structures.h
@@ -0,0 +1,38 @@
+//* 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.
+#include "dawn/webgpu.h"
+
+namespace dawn::kotlin_api {
+
+// Converts Kotlin objects representing Dawn structures into native structures that can be passed
+// into the native Dawn API.
+
+{% for structure in by_category['structure'] %}
+    void Convert(JNIEnv *env, jobject obj, {{ as_cType(structure.name) }}* converted);
+{% endfor %}
+
+}  // namespace dawn::kotlin_api
diff --git a/src/dawn/dawn_kotlin.json b/src/dawn/dawn_kotlin.json
index 80a8cde..1e9dfa0 100644
--- a/src/dawn/dawn_kotlin.json
+++ b/src/dawn/dawn_kotlin.json
@@ -30,5 +30,15 @@
 
     "_doc": "See docs/dawn/codegen.md",
 
-    "kotlin_package": "android.dawn"
+    "kotlin_package": "android.dawn",
+
+    "to_jni_type" : {
+        "bool": "jboolean",
+        "float": "jfloat",
+        "int32_t": "jint",
+        "size_t": "jlong",
+        "uint32_t": "jint",
+        "uint64_t": "jlong",
+        "void": "void"
+    }
 }
diff --git a/tools/android/BUILD.gn b/tools/android/BUILD.gn
index b5b9cd6..ccec004 100644
--- a/tools/android/BUILD.gn
+++ b/tools/android/BUILD.gn
@@ -30,6 +30,9 @@
 dawn_json_generator("kotlin_gen") {
   target = "kotlin"
   outputs = [
+    "cpp/methods.cpp",
+    "cpp/structures.cpp",
+    "cpp/structures.h",
     "java/android/dawn/Adapter.kt",
     "java/android/dawn/AdapterInfo.kt",
     "java/android/dawn/AdapterProperties.kt",
diff --git a/tools/android/webgpu/src/main/cpp/CMakeLists.txt b/tools/android/webgpu/src/main/cpp/CMakeLists.txt
index 4ec0b42..30d0216 100644
--- a/tools/android/webgpu/src/main/cpp/CMakeLists.txt
+++ b/tools/android/webgpu/src/main/cpp/CMakeLists.txt
@@ -43,7 +43,7 @@
 
 target_compile_definitions(webgpu_dawn PRIVATE WGPU_IMPLEMENTATION WGPU_SHARED_LIBRARY)
 
-add_library(native_dawn Util.cpp ${CMAKE_SOURCE_DIR}/gen/include/dawn/webgpu.h)
+add_library(native_dawn methods.cpp structures.cpp utils.cpp ${CMAKE_SOURCE_DIR}/gen/include/dawn/webgpu.h)
 
 # It is not clear why abs::raw_hash_set is not part of the bundled dependency tree naturally.
 bundle_libraries(webgpu_c_bundled webgpu_dawn native_dawn absl::raw_hash_set)