| // Copyright 2021 The Dawn Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // This file provides core interop helpers used by the code generated by the |
| // templates. |
| |
| #ifndef DAWN_NODE_INTEROP_CORE_WEBGPU_H_ |
| #define DAWN_NODE_INTEROP_CORE_WEBGPU_H_ |
| |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| #include <type_traits> |
| #include <unordered_map> |
| #include <variant> |
| #include <vector> |
| |
| #include "napi.h" |
| |
| #include "src/dawn_node/utils/Debug.h" |
| |
| #define ENABLE_INTEROP_LOGGING 0 // Enable for verbose interop logging |
| |
| #if ENABLE_INTEROP_LOGGING |
| # define INTEROP_LOG(...) LOG(__VA_ARGS__) |
| #else |
| # define INTEROP_LOG(...) |
| #endif |
| |
| // A helper macro for constructing a PromiseInfo with the current file, function and line. |
| // See PromiseInfo |
| #define PROMISE_INFO \ |
| ::wgpu::interop::PromiseInfo { \ |
| __FILE__, __FUNCTION__, __LINE__ \ |
| } |
| |
| namespace wgpu { namespace interop { |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Primitive JavaScript types |
| //////////////////////////////////////////////////////////////////////////////// |
| using Object = Napi::Object; |
| using ArrayBuffer = Napi::ArrayBuffer; |
| using Int8Array = Napi::TypedArrayOf<int8_t>; |
| using Int16Array = Napi::TypedArrayOf<int16_t>; |
| using Int32Array = Napi::TypedArrayOf<int32_t>; |
| using Uint8Array = Napi::TypedArrayOf<uint8_t>; |
| using Uint16Array = Napi::TypedArrayOf<uint16_t>; |
| using Uint32Array = Napi::TypedArrayOf<uint32_t>; |
| using Float32Array = Napi::TypedArrayOf<float>; |
| using Float64Array = Napi::TypedArrayOf<double>; |
| using DataView = Napi::TypedArray; |
| |
| template <typename T> |
| using FrozenArray = std::vector<T>; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Result |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // Result is used to hold an success / error state by functions that perform JS <-> C++ |
| // conversion |
| struct [[nodiscard]] Result { |
| // Returns true if the operation succeeded, false if there was an error |
| inline operator bool() const { |
| return error.empty(); |
| } |
| |
| // If Result is an error, then a new Error is returned with the |
| // stringified values append to the error message. |
| // If Result is a success, then a success Result is returned. |
| template <typename... VALUES> |
| Result Append(VALUES && ... values) { |
| if (*this) { |
| return *this; |
| } |
| std::stringstream ss; |
| ss << error << "\n"; |
| utils::Write(ss, std::forward<VALUES>(values)...); |
| return {ss.str()}; |
| } |
| |
| // The error message, if the operation failed. |
| std::string error; |
| }; |
| |
| // A successful result |
| extern Result Success; |
| |
| // Returns a Result with the given error message |
| Result Error(std::string msg); |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Interface<T> |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // Interface<T> is a templated wrapper around a JavaScript object, which |
| // implements the template-generated interface type T. Interfaces are returned |
| // by either calling T::Bind() or T::Create(). |
| template <typename T> |
| class Interface { |
| public: |
| // Constructs an Interface with no JS object. |
| inline Interface() { |
| } |
| |
| // Constructs an Interface wrapping the given JS object. |
| // The JS object must have been created with a call to T::Bind(). |
| explicit inline Interface(Napi::Object o) : object(o) { |
| } |
| |
| // Implicit conversion operators to Napi objects. |
| inline operator napi_value() const { |
| return object; |
| } |
| inline operator const Napi::Value &() const { |
| return object; |
| } |
| inline operator const Napi::Object &() const { |
| return object; |
| } |
| |
| // Member and dereference operators |
| inline T* operator->() const { |
| return T::Unwrap(object); |
| } |
| inline T* operator*() const { |
| return T::Unwrap(object); |
| } |
| |
| // As<IMPL>() returns the unwrapped object cast to the implementation type. |
| // The interface implementation *must* be of the template type IMPL. |
| template <typename IMPL> |
| inline IMPL* As() const { |
| return static_cast<IMPL*>(T::Unwrap(object)); |
| } |
| |
| private: |
| Napi::Object object; |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Promise<T> |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // Info holds details about where the promise was constructed. |
| // Used for printing debug messages when a promise is finalized without being resolved |
| // or rejected. |
| // Use the PROMISE_INFO macro to populate this structure. |
| struct PromiseInfo { |
| const char* file = nullptr; |
| const char* function = nullptr; |
| int line = 0; |
| }; |
| |
| namespace detail { |
| // Base class for Promise<T> specializations. |
| class PromiseBase { |
| public: |
| // Implicit conversion operators to Napi promises. |
| inline operator napi_value() const { |
| return state->deferred.Promise(); |
| } |
| inline operator Napi::Value() const { |
| return state->deferred.Promise(); |
| } |
| inline operator Napi::Promise() const { |
| return state->deferred.Promise(); |
| } |
| |
| // Reject() rejects the promise with the given failure value. |
| void Reject(Napi::Value value) const { |
| state->deferred.Reject(value); |
| state->resolved_or_rejected = true; |
| } |
| void Reject(Napi::Error err) const { |
| Reject(err.Value()); |
| } |
| void Reject(std::string err) const { |
| Reject(Napi::Error::New(state->deferred.Env(), err)); |
| } |
| |
| protected: |
| void Resolve(Napi::Value value) const { |
| state->deferred.Resolve(value); |
| state->resolved_or_rejected = true; |
| } |
| |
| struct State { |
| Napi::Promise::Deferred deferred; |
| PromiseInfo info; |
| bool resolved_or_rejected = false; |
| }; |
| |
| PromiseBase(Napi::Env env, const PromiseInfo& info) |
| : state(new State{Napi::Promise::Deferred::New(env), info}) { |
| state->deferred.Promise().AddFinalizer( |
| [](Napi::Env, State* state) { |
| // TODO(https://github.com/gpuweb/cts/issues/784): |
| // Devices are never destroyed, so we always end up |
| // leaking the Device.lost promise. Enable this once |
| // fixed. |
| if ((false)) { |
| if (!state->resolved_or_rejected) { |
| ::wgpu::utils::Fatal("Promise not resolved or rejected", |
| state->info.file, state->info.line, |
| state->info.function); |
| } |
| } |
| delete state; |
| }, |
| state); |
| } |
| |
| State* const state; |
| }; |
| } // namespace detail |
| |
| // Promise<T> is a templated wrapper around a JavaScript promise, which can |
| // resolve to the template type T. |
| template <typename T> |
| class Promise : public detail::PromiseBase { |
| public: |
| // Constructor |
| Promise(Napi::Env env, const PromiseInfo& info) : PromiseBase(env, info) { |
| } |
| |
| // Resolve() fulfills the promise with the given value. |
| void Resolve(T&& value) const { |
| PromiseBase::Resolve(ToJS(state->deferred.Env(), std::forward<T>(value))); |
| } |
| }; |
| |
| // Specialization for Promises that resolve with no value |
| template <> |
| class Promise<void> : public detail::PromiseBase { |
| public: |
| // Constructor |
| Promise(Napi::Env env, const PromiseInfo& info) : PromiseBase(env, info) { |
| } |
| |
| // Resolve() fulfills the promise. |
| void Resolve() const { |
| PromiseBase::Resolve(state->deferred.Env().Undefined()); |
| } |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Converter<T> |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // Converter<T> is specialized for each type T which can be converted from C++ |
| // to JavaScript, or JavaScript to C++. |
| // Each specialization of Converter<T> is expected to have two static methods |
| // with the signatures: |
| // |
| // // FromJS() converts the JavaScript value 'in' to the C++ value 'out'. |
| // static Result FromJS(Napi::Env, Napi::Value in, T& out); |
| // |
| // // ToJS() converts the C++ value 'in' to a JavaScript value, and returns |
| // // this value. |
| // static Napi::Value ToJS(Napi::Env, T in); |
| template <typename T> |
| class Converter {}; |
| |
| template <> |
| class Converter<Napi::Object> { |
| public: |
| static inline Result FromJS(Napi::Env, Napi::Value value, Napi::Object& out) { |
| if (value.IsObject()) { |
| out = value.ToObject(); |
| return Success; |
| } |
| return Error("value is not an object"); |
| } |
| static inline Napi::Value ToJS(Napi::Env, Napi::Object value) { |
| return value; |
| } |
| }; |
| |
| template <> |
| class Converter<ArrayBuffer> { |
| public: |
| static inline Result FromJS(Napi::Env, Napi::Value value, ArrayBuffer& out) { |
| if (value.IsArrayBuffer()) { |
| out = value.As<ArrayBuffer>(); |
| return Success; |
| } |
| return Error("value is not a ArrayBuffer"); |
| }; |
| static inline Napi::Value ToJS(Napi::Env, ArrayBuffer value) { |
| return value; |
| } |
| }; |
| |
| template <> |
| class Converter<Napi::TypedArray> { |
| public: |
| static inline Result FromJS(Napi::Env, Napi::Value value, Napi::TypedArray& out) { |
| if (value.IsTypedArray()) { |
| out = value.As<Napi::TypedArray>(); |
| return Success; |
| } |
| return Error("value is not a TypedArray"); |
| }; |
| static inline Napi::Value ToJS(Napi::Env, ArrayBuffer value) { |
| return value; |
| } |
| }; |
| |
| template <typename T> |
| class Converter<Napi::TypedArrayOf<T>> { |
| public: |
| // clang-format off |
| // The Napi element type of T |
| static constexpr napi_typedarray_type element_type = |
| std::is_same<T, int8_t>::value ? napi_int8_array |
| : std::is_same<T, uint8_t>::value ? napi_uint8_array |
| : std::is_same<T, int16_t>::value ? napi_int16_array |
| : std::is_same<T, uint16_t>::value ? napi_uint16_array |
| : std::is_same<T, int32_t>::value ? napi_int32_array |
| : std::is_same<T, uint32_t>::value ? napi_uint32_array |
| : std::is_same<T, float>::value ? napi_float32_array |
| : std::is_same<T, double>::value ? napi_float64_array |
| : std::is_same<T, int64_t>::value ? napi_bigint64_array |
| : std::is_same<T, uint64_t>::value ? napi_biguint64_array |
| : static_cast<napi_typedarray_type>(-1); |
| // clang-format on |
| static_assert(static_cast<int>(element_type) >= 0, |
| "unsupported T type for Napi::TypedArrayOf<T>"); |
| static inline Result FromJS(Napi::Env, Napi::Value value, Napi::TypedArrayOf<T>& out) { |
| if (value.IsTypedArray()) { |
| auto arr = value.As<Napi::TypedArrayOf<T>>(); |
| if (arr.TypedArrayType() == element_type) { |
| out = arr; |
| return Success; |
| } |
| return Error("value is not a TypedArray of the correct element type"); |
| } |
| return Error("value is not a TypedArray"); |
| }; |
| static inline Napi::Value ToJS(Napi::Env, ArrayBuffer value) { |
| return value; |
| } |
| }; |
| |
| template <> |
| class Converter<std::string> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, std::string&); |
| static Napi::Value ToJS(Napi::Env, std::string); |
| }; |
| |
| template <> |
| class Converter<bool> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, bool&); |
| static Napi::Value ToJS(Napi::Env, bool); |
| }; |
| |
| template <> |
| class Converter<int8_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, int8_t&); |
| static Napi::Value ToJS(Napi::Env, int8_t); |
| }; |
| |
| template <> |
| class Converter<uint8_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, uint8_t&); |
| static Napi::Value ToJS(Napi::Env, uint8_t); |
| }; |
| |
| template <> |
| class Converter<int16_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, int16_t&); |
| static Napi::Value ToJS(Napi::Env, int16_t); |
| }; |
| |
| template <> |
| class Converter<uint16_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, uint16_t&); |
| static Napi::Value ToJS(Napi::Env, uint16_t); |
| }; |
| |
| template <> |
| class Converter<int32_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, int32_t&); |
| static Napi::Value ToJS(Napi::Env, int32_t); |
| }; |
| |
| template <> |
| class Converter<uint32_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, uint32_t&); |
| static Napi::Value ToJS(Napi::Env, uint32_t); |
| }; |
| |
| template <> |
| class Converter<int64_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, int64_t&); |
| static Napi::Value ToJS(Napi::Env, int64_t); |
| }; |
| |
| template <> |
| class Converter<uint64_t> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, uint64_t&); |
| static Napi::Value ToJS(Napi::Env, uint64_t); |
| }; |
| |
| template <> |
| class Converter<float> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, float&); |
| static Napi::Value ToJS(Napi::Env, float); |
| }; |
| |
| template <> |
| class Converter<double> { |
| public: |
| static Result FromJS(Napi::Env, Napi::Value, double&); |
| static Napi::Value ToJS(Napi::Env, double); |
| }; |
| |
| template <typename T> |
| class Converter<Interface<T>> { |
| public: |
| static Result FromJS(Napi::Env env, Napi::Value value, Interface<T>& out) { |
| if (!value.IsObject()) { |
| return Error("value is not object"); |
| } |
| auto obj = value.As<Napi::Object>(); |
| if (!T::Unwrap(obj)) { |
| return Error("object is not of the correct interface type"); |
| } |
| out = Interface<T>(obj); |
| return Success; |
| } |
| static Napi::Value ToJS(Napi::Env env, const Interface<T>& value) { |
| return {env, value}; |
| } |
| }; |
| |
| template <typename T> |
| class Converter<std::optional<T>> { |
| public: |
| static Result FromJS(Napi::Env env, Napi::Value value, std::optional<T>& out) { |
| if (value.IsNull() || value.IsUndefined()) { |
| out.reset(); |
| return Success; |
| } |
| T v{}; |
| auto res = Converter<T>::FromJS(env, value, v); |
| if (!res) { |
| return res; |
| } |
| out = std::move(v); |
| return Success; |
| } |
| static Napi::Value ToJS(Napi::Env env, std::optional<T> value) { |
| if (value.has_value()) { |
| return Converter<T>::ToJS(env, value.value()); |
| } |
| return env.Null(); |
| } |
| }; |
| |
| template <typename T> |
| class Converter<std::vector<T>> { |
| public: |
| static inline Result FromJS(Napi::Env env, Napi::Value value, std::vector<T>& out) { |
| if (!value.IsArray()) { |
| return Error("value is not an array"); |
| } |
| auto arr = value.As<Napi::Array>(); |
| std::vector<T> vec(arr.Length()); |
| for (size_t i = 0; i < vec.size(); i++) { |
| auto res = Converter<T>::FromJS(env, arr[static_cast<uint32_t>(i)], vec[i]); |
| if (!res) { |
| return res.Append("for array element ", i); |
| } |
| } |
| out = std::move(vec); |
| return Success; |
| } |
| static inline Napi::Value ToJS(Napi::Env env, const std::vector<T>& vec) { |
| auto arr = Napi::Array::New(env, vec.size()); |
| for (size_t i = 0; i < vec.size(); i++) { |
| arr.Set(static_cast<uint32_t>(i), Converter<T>::ToJS(env, vec[i])); |
| } |
| return arr; |
| } |
| }; |
| |
| template <typename K, typename V> |
| class Converter<std::unordered_map<K, V>> { |
| public: |
| static inline Result FromJS(Napi::Env env, |
| Napi::Value value, |
| std::unordered_map<K, V>& out) { |
| if (!value.IsObject()) { |
| return Error("value is not an object"); |
| } |
| auto obj = value.ToObject(); |
| auto keys = obj.GetPropertyNames(); |
| std::unordered_map<K, V> map(keys.Length()); |
| for (uint32_t i = 0; i < static_cast<uint32_t>(keys.Length()); i++) { |
| K key{}; |
| V value{}; |
| auto key_res = Converter<K>::FromJS(env, keys[i], key); |
| if (!key_res) { |
| return key_res.Append("for object key"); |
| } |
| auto value_res = Converter<V>::FromJS(env, obj.Get(keys[i]), value); |
| if (!value_res) { |
| return value_res.Append("for object value of key: ", key); |
| } |
| map[key] = value; |
| } |
| out = std::move(map); |
| return Success; |
| } |
| static inline Napi::Value ToJS(Napi::Env env, std::unordered_map<K, V> value) { |
| auto obj = Napi::Object::New(env); |
| for (auto it : value) { |
| obj.Set(Converter<K>::ToJS(env, it.first), Converter<V>::ToJS(env, it.second)); |
| } |
| return obj; |
| } |
| }; |
| |
| template <typename... TYPES> |
| class Converter<std::variant<TYPES...>> { |
| template <typename TY> |
| static inline Result TryFromJS(Napi::Env env, |
| Napi::Value value, |
| std::variant<TYPES...>& out) { |
| TY v{}; |
| auto res = Converter<TY>::FromJS(env, value, v); |
| if (!res) { |
| return Error("no possible types matched"); |
| } |
| out = std::move(v); |
| return Success; |
| } |
| |
| template <typename T0, typename T1, typename... TN> |
| static inline Result TryFromJS(Napi::Env env, |
| Napi::Value value, |
| std::variant<TYPES...>& out) { |
| if (TryFromJS<T0>(env, value, out)) { |
| return Success; |
| } |
| return TryFromJS<T1, TN...>(env, value, out); |
| } |
| |
| public: |
| static inline Result FromJS(Napi::Env env, Napi::Value value, std::variant<TYPES...>& out) { |
| return TryFromJS<TYPES...>(env, value, out); |
| } |
| static inline Napi::Value ToJS(Napi::Env env, std::variant<TYPES...> value) { |
| return std::visit( |
| [&](auto&& v) { |
| using T = std::remove_cv_t<std::remove_reference_t<decltype(v)>>; |
| return Converter<T>::ToJS(env, v); |
| }, |
| value); |
| } |
| }; |
| |
| template <typename T> |
| class Converter<Promise<T>> { |
| public: |
| static inline Result FromJS(Napi::Env, Napi::Value, Promise<T>&) { |
| UNIMPLEMENTED(); |
| } |
| static inline Napi::Value ToJS(Napi::Env, Promise<T> promise) { |
| return promise; |
| } |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Helpers |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // FromJS() is a helper function which delegates to |
| // Converter<T>::FromJS() |
| template <typename T> |
| inline Result FromJS(Napi::Env env, Napi::Value value, T& out) { |
| return Converter<T>::FromJS(env, value, out); |
| } |
| |
| // FromJSOptional() is similar to FromJS(), but if 'value' is either null |
| // or undefined then 'out' is left unassigned. |
| template <typename T> |
| inline Result FromJSOptional(Napi::Env env, Napi::Value value, T& out) { |
| if (value.IsNull() || value.IsUndefined()) { |
| return Success; |
| } |
| return Converter<T>::FromJS(env, value, out); |
| } |
| |
| // ToJS() is a helper function which delegates to Converter<T>::ToJS() |
| template <typename T> |
| inline Napi::Value ToJS(Napi::Env env, T&& value) { |
| return Converter<std::remove_cv_t<std::remove_reference_t<T>>>::ToJS( |
| env, std::forward<T>(value)); |
| } |
| |
| // DefaultedParameter can be used in the tuple parameter types passed to |
| // FromJS(const Napi::CallbackInfo& info, PARAM_TYPES& args), for parameters |
| // that have a default value. If the argument is omitted in the call, then |
| // DefaultedParameter::default_value will be assigned to |
| // DefaultedParameter::value. |
| template <typename T> |
| struct DefaultedParameter { |
| T value; // The argument value assigned by FromJS() |
| T default_value; // The default value if no argument supplied |
| |
| // Implicit conversion operator. Returns value. |
| inline operator const T&() const { |
| return value; |
| } |
| }; |
| |
| // IsDefaultedParameter<T>::value is true iff T is of type DefaultedParameter. |
| template <typename T> |
| struct IsDefaultedParameter { |
| static constexpr bool value = false; |
| }; |
| template <typename T> |
| struct IsDefaultedParameter<DefaultedParameter<T>> { |
| static constexpr bool value = true; |
| }; |
| |
| // FromJS() is a helper function for bulk converting the arguments of 'info'. |
| // PARAM_TYPES is a std::tuple<> describing the C++ function parameter types. |
| // Parameters may be of the templated DefaultedParameter type, in which case |
| // the parameter will default to the default-value if omitted. |
| template <typename PARAM_TYPES, int BASE_INDEX = 0> |
| inline Result FromJS(const Napi::CallbackInfo& info, PARAM_TYPES& args) { |
| if constexpr (BASE_INDEX < std::tuple_size_v<PARAM_TYPES>) { |
| using T = std::tuple_element_t<BASE_INDEX, PARAM_TYPES>; |
| auto& value = info[BASE_INDEX]; |
| auto& out = std::get<BASE_INDEX>(args); |
| if constexpr (IsDefaultedParameter<T>::value) { |
| // Parameter has a default value. |
| // Check whether the argument was provided. |
| if (value.IsNull() || value.IsUndefined()) { |
| // Use default value for this parameter |
| out.value = out.default_value; |
| } else { |
| // Argument was provided |
| auto res = FromJS(info.Env(), value, out.value); |
| if (!res) { |
| return res; |
| } |
| } |
| } else { |
| // Parameter does not have a default value. |
| auto res = FromJS(info.Env(), value, out); |
| if (!res) { |
| return res; |
| } |
| } |
| // Convert the rest of the arguments |
| return FromJS<PARAM_TYPES, BASE_INDEX + 1>(info, args); |
| } else { |
| return Success; |
| } |
| } |
| |
| }} // namespace wgpu::interop |
| |
| #endif // DAWN_NODE_INTEROP_CORE_WEBGPU_H_ |