| // Copyright 2022 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. |
| |
| #ifndef SRC_DAWN_NATIVE_STREAM_STREAM_H_ |
| #define SRC_DAWN_NATIVE_STREAM_STREAM_H_ |
| |
| #include <algorithm> |
| #include <bitset> |
| #include <functional> |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <unordered_map> |
| #include <unordered_set> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/container/flat_hash_set.h" |
| #include "dawn/common/Platform.h" |
| #include "dawn/common/TypedInteger.h" |
| #include "dawn/common/ityp_array.h" |
| #include "dawn/native/Error.h" |
| #include "dawn/native/stream/Sink.h" |
| #include "dawn/native/stream/Source.h" |
| |
| namespace dawn::ityp { |
| template <typename Index, size_t N> |
| class bitset; |
| } // namespace dawn::ityp |
| |
| namespace dawn::native::stream { |
| |
| // Base Stream template for specialization. Specializations may define static methods: |
| // static void Write(Sink* s, const T& v); |
| // static MaybeError Read(Source* s, T* v); |
| template <typename T, typename SFINAE = void> |
| class Stream { |
| public: |
| static void Write(Sink* s, const T& v); |
| static MaybeError Read(Source* s, T* v); |
| }; |
| |
| // Helper to call Stream<T>::Write. |
| // By default, calling StreamIn will call this overload inside the stream namespace. |
| // Other definitons of StreamIn found by ADL may override this one. |
| template <typename T> |
| constexpr void StreamIn(Sink* s, const T& v) { |
| Stream<T>::Write(s, v); |
| } |
| |
| // Helper to call Stream<T>::Read |
| // By default, calling StreamOut will call this overload inside the stream namespace. |
| // Other definitons of StreamOut found by ADL may override this one. |
| template <typename T> |
| MaybeError StreamOut(Source* s, T* v) { |
| return Stream<T>::Read(s, v); |
| } |
| |
| // Helper to take an rvalue passed to StreamOut and forward it as a pointer. |
| // This makes it possible to pass output wrappers like stream::StructMembers inline. |
| // For example: `DAWN_TRY(StreamOut(&source, stream::StructMembers(...)));` |
| template <typename T> |
| MaybeError StreamOut(Source* s, T&& v) { |
| return StreamOut(s, &v); |
| } |
| |
| // Helper to call StreamIn on a parameter pack. |
| template <typename T, typename... Ts> |
| constexpr void StreamIn(Sink* s, const T& v, const Ts&... vs) { |
| StreamIn(s, v); |
| (StreamIn(s, vs), ...); |
| } |
| |
| // Helper to call StreamOut on a parameter pack. |
| template <typename T, typename... Ts> |
| MaybeError StreamOut(Source* s, T* v, Ts*... vs) { |
| DAWN_TRY(StreamOut(s, v)); |
| return StreamOut(s, vs...); |
| } |
| |
| // Helper to call StreamIn on an empty parameter pack, e.g. for a DAWN_SERIALIZABLE struct with no |
| // member. Do nothing. |
| inline constexpr void StreamIn(Sink* s) {} |
| |
| // Helper to call StreamOut on an empty parameter pack, e.g. for a DAWN_SERIALIZABLE struct with no |
| // member. Do nothing and return success. |
| inline MaybeError StreamOut(Source* s) { |
| return {}; |
| } |
| |
| // Stream specialization for fundamental types. |
| template <typename T> |
| class Stream<T, std::enable_if_t<std::is_fundamental_v<T>>> { |
| public: |
| static void Write(Sink* s, const T& v) { memcpy(s->GetSpace(sizeof(T)), &v, sizeof(T)); } |
| static MaybeError Read(Source* s, T* v) { |
| const void* ptr; |
| DAWN_TRY(s->Read(&ptr, sizeof(T))); |
| memcpy(v, ptr, sizeof(T)); |
| return {}; |
| } |
| }; |
| |
| namespace detail { |
| // NOLINTNEXTLINE(runtime/int) Alias "unsigned long long" type to match std::bitset to_ullong |
| using BitsetUllong = unsigned long long; |
| constexpr size_t kBitsPerUllong = 8 * sizeof(BitsetUllong); |
| constexpr bool BitsetSupportsToUllong(size_t N) { |
| return N <= kBitsPerUllong; |
| } |
| } // namespace detail |
| |
| // Stream specialization for bitsets that are smaller than BitsetUllong. |
| template <size_t N> |
| class Stream<std::bitset<N>, std::enable_if_t<detail::BitsetSupportsToUllong(N)>> { |
| public: |
| static void Write(Sink* s, const std::bitset<N>& t) { StreamIn(s, t.to_ullong()); } |
| static MaybeError Read(Source* s, std::bitset<N>* v) { |
| detail::BitsetUllong value; |
| DAWN_TRY(StreamOut(s, &value)); |
| *v = std::bitset<N>(value); |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for bitsets since using the built-in to_ullong has a size limit. |
| template <size_t N> |
| class Stream<std::bitset<N>, std::enable_if_t<!detail::BitsetSupportsToUllong(N)>> { |
| public: |
| static void Write(Sink* s, const std::bitset<N>& t) { |
| // Iterate in chunks of detail::BitsetUllong. |
| static std::bitset<N> mask(std::numeric_limits<detail::BitsetUllong>::max()); |
| |
| std::bitset<N> bits = t; |
| for (size_t offset = 0; offset < N; |
| offset += detail::kBitsPerUllong, bits >>= detail::kBitsPerUllong) { |
| StreamIn(s, (mask & bits).to_ullong()); |
| } |
| } |
| |
| static MaybeError Read(Source* s, std::bitset<N>* v) { |
| static_assert(N > 0); |
| *v = {}; |
| |
| // Iterate in chunks of detail::BitsetUllong. |
| for (size_t offset = 0; offset < N; |
| offset += detail::kBitsPerUllong, (*v) <<= detail::kBitsPerUllong) { |
| detail::BitsetUllong ullong; |
| DAWN_TRY(StreamOut(s, &ullong)); |
| *v |= std::bitset<N>(ullong); |
| } |
| return {}; |
| } |
| }; |
| |
| template <typename Index, size_t N> |
| class Stream<ityp::bitset<Index, N>> { |
| public: |
| static void Write(Sink* s, const ityp::bitset<Index, N>& v) { StreamIn(s, v.AsBase()); } |
| static MaybeError Read(Source* s, ityp::bitset<Index, N>* v) { |
| return StreamOut(s, &v->AsBase()); |
| } |
| }; |
| |
| // Stream specialization for enums. |
| template <typename T> |
| class Stream<T, std::enable_if_t<std::is_enum_v<T>>> { |
| using U = std::underlying_type_t<T>; |
| |
| public: |
| static void Write(Sink* s, const T& v) { StreamIn(s, static_cast<U>(v)); } |
| |
| static MaybeError Read(Source* s, T* v) { |
| U out; |
| DAWN_TRY(StreamOut(s, &out)); |
| *v = static_cast<T>(out); |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for TypedInteger. |
| template <typename Tag, typename Integer> |
| class Stream<::dawn::detail::TypedIntegerImpl<Tag, Integer>> { |
| using T = ::dawn::detail::TypedIntegerImpl<Tag, Integer>; |
| |
| public: |
| static void Write(Sink* s, const T& t) { StreamIn(s, static_cast<Integer>(t)); } |
| |
| static MaybeError Read(Source* s, T* v) { |
| Integer out; |
| DAWN_TRY(StreamOut(s, &out)); |
| *v = T(out); |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for pointers. We always serialize via value, not by pointer. |
| // To handle nullptr scenarios, we always serialize whether the pointer was not nullptr, |
| // followed by the contents if applicable. |
| template <typename T> |
| class Stream<T, std::enable_if_t<std::is_pointer_v<T>>> { |
| public: |
| static void Write(stream::Sink* sink, const T& t) { |
| using Pointee = std::decay_t<std::remove_pointer_t<T>>; |
| static_assert(!std::is_same_v<char, Pointee> && !std::is_same_v<wchar_t, Pointee> && |
| !std::is_same_v<char16_t, Pointee> && !std::is_same_v<char32_t, Pointee>, |
| "C-str like type likely has ambiguous serialization. For a string, wrap with " |
| "std::string_view instead."); |
| StreamIn(sink, t != nullptr); |
| if (t != nullptr) { |
| StreamIn(sink, *t); |
| } |
| } |
| }; |
| |
| // Stream specialization for unique pointers. We always serialize/deserialize via value, not by |
| // pointer. To handle nullptr scenarios, we always serialize whether the pointer was not nullptr, |
| // followed by the contents if applicable. |
| template <typename T> |
| class Stream<std::unique_ptr<T>, std::enable_if_t<!std::is_pointer_v<T>>> { |
| public: |
| static void Write(stream::Sink* sink, const std::unique_ptr<T>& t) { |
| StreamIn(sink, t != nullptr); |
| if (t != nullptr) { |
| StreamIn(sink, *t); |
| } |
| } |
| |
| static MaybeError Read(Source* source, std::unique_ptr<T>* t) { |
| bool notNullptr; |
| DAWN_TRY(StreamOut(source, ¬Nullptr)); |
| if (notNullptr) { |
| // Avoid using copy or move constructor of T. |
| std::unique_ptr<T> out = std::make_unique<T>(); |
| DAWN_TRY(StreamOut(source, out.get())); |
| *t = std::move(out); |
| } else { |
| *t = nullptr; |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for reference_wrapper. For serialization, unwrap it to the reference const |
| // T& and call Write(sink, const T&). |
| template <typename T> |
| class Stream<std::reference_wrapper<T>> { |
| public: |
| static void Write(stream::Sink* sink, const std::reference_wrapper<T>& t) { |
| StreamIn(sink, t.get()); |
| } |
| }; |
| |
| // Stream specialization for std::optional |
| template <typename T> |
| class Stream<std::optional<T>> { |
| public: |
| static void Write(stream::Sink* sink, const std::optional<T>& t) { |
| bool hasValue = t.has_value(); |
| StreamIn(sink, hasValue); |
| if (hasValue) { |
| StreamIn(sink, *t); |
| } |
| } |
| static MaybeError Read(Source* source, std::optional<T>* t) { |
| bool hasValue; |
| DAWN_TRY(StreamOut(source, &hasValue)); |
| if (hasValue) { |
| T out; |
| DAWN_TRY(StreamOut(source, &out)); |
| *t = std::move(out); |
| } else { |
| t->reset(); |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for fixed arrays of fundamental types. |
| template <typename T, size_t N> |
| class Stream<T[N], std::enable_if_t<std::is_fundamental_v<T>>> { |
| public: |
| static void Write(Sink* s, const T (&t)[N]) { |
| static_assert(N > 0); |
| memcpy(s->GetSpace(sizeof(t)), &t, sizeof(t)); |
| } |
| |
| static MaybeError Read(Source* s, T (*t)[N]) { |
| static_assert(N > 0); |
| const void* ptr; |
| DAWN_TRY(s->Read(&ptr, sizeof(*t))); |
| memcpy(*t, ptr, sizeof(*t)); |
| return {}; |
| } |
| }; |
| |
| // Specialization for fixed arrays of non-fundamental types. |
| template <typename T, size_t N> |
| class Stream<T[N], std::enable_if_t<!std::is_fundamental_v<T>>> { |
| public: |
| static void Write(Sink* s, const T (&t)[N]) { |
| static_assert(N > 0); |
| for (size_t i = 0; i < N; i++) { |
| StreamIn(s, t[i]); |
| } |
| } |
| |
| static MaybeError Read(Source* s, T (*t)[N]) { |
| static_assert(N > 0); |
| for (size_t i = 0; i < N; i++) { |
| DAWN_TRY(StreamOut(s, &(*t)[i])); |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for std::vector. |
| template <typename T> |
| class Stream<std::vector<T>> { |
| public: |
| static void Write(Sink* s, const std::vector<T>& v) { |
| StreamIn(s, v.size()); |
| for (const T& it : v) { |
| StreamIn(s, it); |
| } |
| } |
| |
| static MaybeError Read(Source* s, std::vector<T>* v) { |
| using SizeT = decltype(std::declval<std::vector<T>>().size()); |
| SizeT size; |
| DAWN_TRY(StreamOut(s, &size)); |
| DAWN_INVALID_IF(size >= v->max_size(), |
| "Trying to reserve a vector of size larger than max_size"); |
| *v = {}; |
| v->reserve(size); |
| for (SizeT i = 0; i < size; ++i) { |
| T el; |
| DAWN_TRY(StreamOut(s, &el)); |
| v->push_back(std::move(el)); |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for std::array<T, Size> of fundamental types T. |
| template <typename T, size_t Size> |
| class Stream<std::array<T, Size>, std::enable_if_t<std::is_fundamental_v<T>>> { |
| public: |
| static void Write(Sink* s, const std::array<T, Size>& t) { |
| static_assert(Size > 0); |
| memcpy(s->GetSpace(sizeof(t)), t.data(), sizeof(t)); |
| } |
| |
| static MaybeError Read(Source* s, std::array<T, Size>* t) { |
| static_assert(Size > 0); |
| const void* ptr; |
| DAWN_TRY(s->Read(&ptr, sizeof(*t))); |
| memcpy(t->data(), ptr, sizeof(*t)); |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for std::array<T, Size> of non-fundamental types T. |
| template <typename T, size_t Size> |
| class Stream<std::array<T, Size>, std::enable_if_t<!std::is_fundamental_v<T>>> { |
| public: |
| static void Write(Sink* s, const std::array<T, Size>& v) { |
| static_assert(Size > 0); |
| for (const T& it : v) { |
| StreamIn(s, it); |
| } |
| } |
| |
| static MaybeError Read(Source* s, std::array<T, Size>* v) { |
| static_assert(Size > 0); |
| for (auto& el : *v) { |
| DAWN_TRY(StreamOut(s, el)); |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for ityp::array<Index, Value, Size>. |
| template <typename Index, typename Value, size_t Size> |
| class Stream<ityp::array<Index, Value, Size>> { |
| public: |
| using ArrayType = ityp::array<Index, Value, Size>; |
| |
| static void Write(Sink* s, const ArrayType& v) { |
| for (const Value& it : v) { |
| StreamIn(s, it); |
| } |
| } |
| |
| static MaybeError Read(Source* s, ArrayType* v) { |
| for (auto& el : *v) { |
| DAWN_TRY(StreamOut(s, el)); |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for std::pair. |
| template <typename A, typename B> |
| class Stream<std::pair<A, B>> { |
| public: |
| static void Write(Sink* s, const std::pair<A, B>& v) { |
| StreamIn(s, v.first); |
| StreamIn(s, v.second); |
| } |
| |
| static MaybeError Read(Source* s, std::pair<A, B>* v) { |
| DAWN_TRY(StreamOut(s, &v->first)); |
| DAWN_TRY(StreamOut(s, &v->second)); |
| return {}; |
| } |
| }; |
| |
| template <typename M> |
| concept IsMapLike = std::is_same_v<M, |
| std::unordered_map<typename M::key_type, |
| typename M::mapped_type, |
| typename M::hasher, |
| typename M::key_equal, |
| typename M::allocator_type>> || |
| std::is_same_v<M, |
| absl::flat_hash_map<typename M::key_type, |
| typename M::mapped_type, |
| typename M::hasher, |
| typename M::key_equal, |
| typename M::allocator_type>>; |
| |
| // Stream specialization for std::unordered_map<K, V> and absl::flat_hash_map which sorts the |
| // entries to provide a stable ordering. |
| template <IsMapLike MapType> |
| class Stream<MapType> { |
| public: |
| using ConstRefWrapper = std::reference_wrapper<const typename MapType::value_type>; |
| using RefVector = std::vector<ConstRefWrapper>; |
| static void Write(stream::Sink* sink, const MapType& m) { |
| // Use a vector of wrapped reference for sorting to avoid copying the elements. |
| RefVector refVector(m.cbegin(), m.cend()); |
| std::sort(refVector.begin(), refVector.end(), |
| [](const ConstRefWrapper& a, const ConstRefWrapper& b) { |
| return a.get().first < b.get().first; |
| }); |
| StreamIn(sink, refVector); |
| } |
| static MaybeError Read(Source* s, MapType* m) { |
| using SizeT = decltype(std::declval<RefVector>().size()); |
| SizeT size; |
| DAWN_TRY(StreamOut(s, &size)); |
| *m = {}; |
| m->reserve(size); |
| for (SizeT i = 0; i < size; ++i) { |
| std::pair<typename MapType::key_type, typename MapType::mapped_type> p; |
| DAWN_TRY(StreamOut(s, &p)); |
| m->insert(std::move(p)); |
| } |
| return {}; |
| } |
| }; |
| |
| template <typename S> |
| concept IsSetLike = std::is_same_v<S, |
| std::unordered_set<typename S::key_type, |
| typename S::hasher, |
| typename S::key_equal, |
| typename S::allocator_type>> || |
| std::is_same_v<S, |
| absl::flat_hash_set<typename S::key_type, |
| typename S::hasher, |
| typename S::key_equal, |
| typename S::allocator_type>>; |
| |
| // Stream specialization for std::unordered_set<V> and absl::flat_hash_set which sorts the entries |
| // to provide a stable ordering. |
| template <IsSetLike SetType> |
| class Stream<SetType> { |
| public: |
| using ConstRefWrapper = std::reference_wrapper<const typename SetType::value_type>; |
| using RefVector = std::vector<ConstRefWrapper>; |
| static void Write(stream::Sink* sink, const SetType& s) { |
| // Use a vector of wrapped reference for sorting to avoid copying the elements. |
| RefVector refVector(s.cbegin(), s.cend()); |
| std::sort( |
| refVector.begin(), refVector.end(), |
| [](const ConstRefWrapper& a, const ConstRefWrapper& b) { return a.get() < b.get(); }); |
| StreamIn(sink, refVector); |
| } |
| static MaybeError Read(Source* source, SetType* s) { |
| using SizeT = decltype(std::declval<RefVector>().size()); |
| SizeT size; |
| DAWN_TRY(StreamOut(source, &size)); |
| *s = {}; |
| s->reserve(size); |
| for (SizeT i = 0; i < size; ++i) { |
| typename SetType::key_type v; |
| DAWN_TRY(StreamOut(source, &v)); |
| s->insert(std::move(v)); |
| } |
| return {}; |
| } |
| }; |
| |
| // Stream specialization for std::variant<Types...> which read/write the type id and the typed |
| // value. |
| template <typename... Types> |
| class Stream<std::variant<Types...>> { |
| public: |
| using VariantType = std::variant<Types...>; |
| |
| static void Write(stream::Sink* sink, const VariantType& t) { WriteImpl<0, Types...>(sink, t); } |
| |
| static MaybeError Read(stream::Source* source, VariantType* t) { |
| size_t typeId; |
| DAWN_TRY(StreamOut(source, &typeId)); |
| if (typeId >= sizeof...(Types)) { |
| return DAWN_VALIDATION_ERROR("Invalid variant type id"); |
| } else { |
| return ReadImpl<0, Types...>(source, t, typeId); |
| } |
| } |
| |
| private: |
| // WriteImpl template for trying multiple possible value types |
| template <size_t N, |
| typename TryType, |
| typename... RemainingTypes, |
| typename = std::enable_if_t<sizeof...(RemainingTypes) != 0>> |
| static inline void WriteImpl(stream::Sink* sink, const VariantType& t) { |
| if (std::holds_alternative<TryType>(t)) { |
| // Record the type index |
| StreamIn(sink, N); |
| // Record the value |
| StreamIn(sink, std::get<TryType>(t)); |
| } else { |
| // Try the next type |
| WriteImpl<N + 1, RemainingTypes...>(sink, t); |
| } |
| } |
| // WriteImpl template for trying the last possible type |
| template <size_t N, typename LastType> |
| static inline void WriteImpl(stream::Sink* sink, const VariantType& t) { |
| // Variant must hold the last possible type if no previous match. |
| DAWN_ASSERT(std::holds_alternative<LastType>(t)); |
| // Record the type index |
| StreamIn(sink, N); |
| // Record the value |
| StreamIn(sink, std::get<LastType>(t)); |
| } |
| // ReadImpl template for trying multiple possible value types |
| template <size_t N, |
| typename TryType, |
| typename... RemainingTypes, |
| typename = std::enable_if_t<sizeof...(RemainingTypes) != 0>> |
| static inline MaybeError ReadImpl(stream::Source* source, VariantType* t, size_t typeId) { |
| if (typeId == N) { |
| // Read the value |
| TryType value; |
| DAWN_TRY(StreamOut(source, &value)); |
| *t = VariantType(std::move(value)); |
| return {}; |
| } else { |
| // Try the next type |
| return ReadImpl<N + 1, RemainingTypes...>(source, t, typeId); |
| } |
| } |
| // ReadImpl template for trying the last possible type |
| template <size_t N, typename LastType> |
| static inline MaybeError ReadImpl(stream::Source* source, VariantType* t, size_t typeId) { |
| // typeId must be the id of last possible type N if not being 0..N-1, since it has been |
| // validated in range 0..N |
| DAWN_ASSERT(typeId == N); |
| // Read the value |
| LastType value; |
| DAWN_TRY(StreamOut(source, &value)); |
| *t = VariantType(std::move(value)); |
| return {}; |
| } |
| }; |
| |
| // Helper class to contain the begin/end iterators of an iterable. |
| namespace detail { |
| template <typename Iterator> |
| struct Iterable { |
| Iterator begin; |
| Iterator end; |
| }; |
| } // namespace detail |
| |
| // Helper for making detail::Iterable from a pointer and count. |
| template <typename T> |
| auto Iterable(const T* ptr, size_t count) { |
| using Iterator = const T*; |
| return detail::Iterable<Iterator>{ptr, ptr + count}; |
| } |
| |
| // Stream specialization for detail::Iterable which writes the number of elements, |
| // followed by the elements. |
| template <typename Iterator> |
| class Stream<detail::Iterable<Iterator>> { |
| public: |
| static void Write(stream::Sink* sink, const detail::Iterable<Iterator>& iter) { |
| StreamIn(sink, std::distance(iter.begin, iter.end)); |
| for (auto it = iter.begin; it != iter.end; ++it) { |
| StreamIn(sink, *it); |
| } |
| } |
| }; |
| |
| } // namespace dawn::native::stream |
| |
| #endif // SRC_DAWN_NATIVE_STREAM_STREAM_H_ |