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" }
+ }
+ }
+ }
+}