| //* 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; |
| }; |
| |
| jobject toByteBuffer(JNIEnv *env, const void* address, jlong size) { |
| if (!address) { |
| return nullptr; |
| } |
| jclass byteBufferClass = env->FindClass("java/nio/ByteBuffer"); |
| |
| //* Dawn always uses little endian format, so we pre-convert for the client's convenience. |
| jclass byteOrderClass = env->FindClass("java/nio/ByteOrder"); |
| jobject littleEndian = env->NewGlobalRef(env->GetStaticObjectField( |
| byteOrderClass, env->GetStaticFieldID(byteOrderClass, "LITTLE_ENDIAN", |
| "Ljava/nio/ByteOrder;"))); |
| |
| jobject byteBuffer = env->NewDirectByteBuffer(const_cast<void *>(address), size); |
| |
| env->CallObjectMethod( |
| byteBuffer, env->GetMethodID(byteBufferClass, "order", |
| "(Ljava/nio/ByteOrder;)Ljava/nio/ByteBuffer;"), |
| littleEndian); |
| return byteBuffer; |
| } |
| |
| {% 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() in ['void const *', 'void *'] %} |
| return toByteBuffer(env, result, size); |
| {% 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 %} |
| |
| //* Every object gets a Release method, to supply a Kotlin AutoCloseable. |
| extern "C" |
| JNIEXPORT void JNICALL |
| Java_{{ kotlin_package.replace('.', '_') }}_{{ obj.name.CamelCase() }}_close( |
| JNIEnv *env, jobject obj) { |
| jclass clz = env->FindClass("{{ jni_name(obj) }}"); |
| const {{ as_cType(obj.name) }} handle = reinterpret_cast<{{ as_cType(obj.name) }}>( |
| env->CallLongMethod(obj, env->GetMethodID(clz, "getHandle", "()J"))); |
| wgpu{{ obj.name.CamelCase() }}Release(handle); |
| } |
| {% 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 |