| #ifndef SRC_ALIASED_STRUCT_H_ |
| #define SRC_ALIASED_STRUCT_H_ |
| |
| #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
| |
| #include <memory> |
| #include <vector> |
| #include "node_internals.h" |
| #include "v8.h" |
| |
| namespace node { |
| |
| // AliasedStruct is a utility that allows uses a V8 Backing Store |
| // to be exposed to the C++/C side as a struct and to the |
| // JavaScript side as an ArrayBuffer to efficiently share |
| // data without marshalling. It is similar in nature to |
| // AliasedBuffer. |
| // |
| // struct Foo { int x; } |
| // |
| // AliasedStruct<Foo> foo; |
| // foo->x = 1; |
| // |
| // Local<ArrayBuffer> ab = foo.GetArrayBuffer(); |
| template <typename T> |
| class AliasedStruct final { |
| public: |
| template <typename... Args> |
| explicit AliasedStruct(v8::Isolate* isolate, Args&&... args); |
| |
| inline AliasedStruct(const AliasedStruct& that); |
| |
| inline ~AliasedStruct(); |
| |
| inline AliasedStruct& operator=(AliasedStruct&& that) noexcept; |
| |
| v8::Local<v8::ArrayBuffer> GetArrayBuffer() const { |
| return buffer_.Get(isolate_); |
| } |
| |
| const T* Data() const { return ptr_; } |
| |
| T* Data() { return ptr_; } |
| |
| const T& operator*() const { return *ptr_; } |
| |
| T& operator*() { return *ptr_; } |
| |
| const T* operator->() const { return ptr_; } |
| |
| T* operator->() { return ptr_; } |
| |
| private: |
| v8::Isolate* isolate_; |
| std::shared_ptr<v8::BackingStore> store_; |
| T* ptr_; |
| v8::Global<v8::ArrayBuffer> buffer_; |
| }; |
| |
| // --------------------------------------------------------------------------- |
| // ArenaSlot — type-erased handle to a slot in an AliasedStructArena page. |
| // This can be stored in headers where T is incomplete. The typed accessors |
| // are provided via a thin typed wrapper (AliasedStructArena<T>::Slot). |
| struct ArenaSlotBase { |
| // Opaque page pointer — only the arena knows the concrete type. |
| void* page = nullptr; |
| void* ptr = nullptr; |
| uint32_t index = 0; |
| size_t byte_offset = 0; |
| |
| explicit operator bool() const { return ptr != nullptr; } |
| |
| // Returns the page's ArrayBuffer. Implemented below after ArenaPageHeader. |
| v8::Local<v8::ArrayBuffer> GetArrayBuffer(v8::Isolate* isolate) const; |
| |
| size_t GetByteOffset() const { return byte_offset; } |
| |
| // Returns the page's cached DataView over the full page. |
| // Callers use byte_offset to index into the correct slot region. |
| v8::Local<v8::DataView> GetPageDataView(v8::Isolate* isolate) const; |
| |
| // Returns the page's cached BigUint64Array over the full page. |
| // Callers use byte_offset / sizeof(uint64_t) to index into the |
| // correct slot region. |
| v8::Local<v8::BigUint64Array> GetPageBigUint64Array( |
| v8::Isolate* isolate) const; |
| }; |
| |
| // --------------------------------------------------------------------------- |
| // AliasedStructArena<T> — pool allocator for AliasedStruct-style shared |
| // memory. Instead of creating a separate ArrayBuffer + BackingStore per |
| // instance, the arena pre-allocates pages of N slots backed by a single |
| // ArrayBuffer each. Callers receive a Slot handle that provides the same |
| // T*/operator-> interface as AliasedStruct, plus the ability to create a |
| // JS typed-array view over just that slot's region of the page buffer. |
| // |
| // Pages target kPageBytes (default 16 KB) for L1 cache residency during |
| // sequential access patterns. Slots are recycled via an intrusive |
| // freelist, and empty pages are dropped when their last slot is released. |
| // |
| // Usage: |
| // AliasedStructArena<MyState> arena; |
| // auto slot = arena.Allocate(isolate); |
| // slot->some_field = 42; |
| // auto view = slot.GetArrayBuffer(isolate); // JS-visible view |
| // ... |
| // arena.Release(std::move(slot)); // return to freelist |
| // |
| template <typename T, size_t kPageBytes = 16384> |
| class AliasedStructArena final { |
| public: |
| static constexpr size_t kSlotsPerPage = kPageBytes / sizeof(T); |
| static_assert(kSlotsPerPage >= 4, "Page too small for type T"); |
| static_assert(sizeof(T) >= sizeof(uint32_t), |
| "T must be at least 4 bytes for freelist linkage"); |
| |
| AliasedStructArena() = default; |
| ~AliasedStructArena() = default; |
| |
| AliasedStructArena(const AliasedStructArena&) = delete; |
| AliasedStructArena& operator=(const AliasedStructArena&) = delete; |
| |
| struct Page { |
| std::shared_ptr<v8::BackingStore> store; |
| v8::Global<v8::ArrayBuffer> buffer; |
| // Lazily created full-page views shared by all slots in |
| // this page. Typically only one is used per arena. |
| v8::Global<v8::DataView> data_view; |
| v8::Global<v8::BigUint64Array> big_uint64_array; |
| size_t page_byte_length = 0; |
| T* base = nullptr; |
| uint32_t free_head = 0; |
| uint32_t used_count = 0; |
| static constexpr uint32_t kNoFreeSlot = UINT32_MAX; |
| |
| void Init(v8::Isolate* isolate) { |
| const v8::HandleScope handle_scope(isolate); |
| const size_t total_bytes = kSlotsPerPage * sizeof(T); |
| store = v8::ArrayBuffer::NewBackingStore(isolate, total_bytes); |
| memset(store->Data(), 0, total_bytes); |
| base = static_cast<T*>(store->Data()); |
| page_byte_length = total_bytes; |
| v8::Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, store); |
| buffer = v8::Global<v8::ArrayBuffer>(isolate, ab); |
| |
| // Build freelist: each slot points to the next. |
| for (uint32_t i = 0; i < kSlotsPerPage - 1; i++) { |
| *reinterpret_cast<uint32_t*>(&base[i]) = i + 1; |
| } |
| *reinterpret_cast<uint32_t*>(&base[kSlotsPerPage - 1]) = kNoFreeSlot; |
| free_head = 0; |
| used_count = 0; |
| } |
| |
| bool HasFreeSlots() const { return free_head != kNoFreeSlot; } |
| }; |
| |
| // Typed slot handle — wraps ArenaSlotBase with T* accessors. |
| class Slot : public ArenaSlotBase { |
| public: |
| Slot() = default; |
| |
| const T& operator*() const { return *static_cast<T*>(ptr); } |
| T& operator*() { return *static_cast<T*>(ptr); } |
| const T* operator->() const { return static_cast<T*>(ptr); } |
| T* operator->() { return static_cast<T*>(ptr); } |
| T* Data() { return static_cast<T*>(ptr); } |
| const T* Data() const { return static_cast<T*>(ptr); } |
| }; |
| |
| // Allocate a slot, placement-constructing T with the given args. |
| // Creates a new page if all existing pages are full. |
| template <typename... Args> |
| Slot Allocate(v8::Isolate* isolate, Args&&... args); |
| |
| // Release a slot back to the arena freelist. Calls ~T() and zeros |
| // the memory so that any JS views see clean data. |
| void Release(Slot&& slot); |
| |
| // Release a slot given a type-erased ArenaSlotBase reference. |
| // Convenience for callers that store ArenaSlotBase in headers where |
| // T is incomplete. |
| void ReleaseSlot(ArenaSlotBase& base); |
| |
| private: |
| Page* FindOrCreatePage(v8::Isolate* isolate); |
| |
| std::vector<std::unique_ptr<Page>> pages_; |
| }; |
| |
| // ArenaSlotBase accessors need to reach the v8::Globals inside a Page. |
| // All AliasedStructArena<T>::Page types share the same leading layout. |
| // The page_byte_length field allows lazy view creation without knowing T. |
| namespace detail { |
| struct ArenaPageHeader { |
| std::shared_ptr<v8::BackingStore> store; |
| v8::Global<v8::ArrayBuffer> buffer; |
| v8::Global<v8::DataView> data_view; |
| v8::Global<v8::BigUint64Array> big_uint64_array; |
| size_t page_byte_length = 0; |
| |
| v8::Local<v8::DataView> GetDataView(v8::Isolate* isolate) { |
| if (data_view.IsEmpty()) { |
| const v8::HandleScope handle_scope(isolate); |
| auto dv = v8::DataView::New(buffer.Get(isolate), 0, page_byte_length); |
| data_view = v8::Global<v8::DataView>(isolate, dv); |
| } |
| return data_view.Get(isolate); |
| } |
| |
| v8::Local<v8::BigUint64Array> GetBigUint64Array(v8::Isolate* isolate) { |
| if (big_uint64_array.IsEmpty()) { |
| const v8::HandleScope handle_scope(isolate); |
| auto bu = v8::BigUint64Array::New( |
| buffer.Get(isolate), 0, page_byte_length / sizeof(uint64_t)); |
| big_uint64_array = v8::Global<v8::BigUint64Array>(isolate, bu); |
| } |
| return big_uint64_array.Get(isolate); |
| } |
| }; |
| } // namespace detail |
| |
| inline v8::Local<v8::ArrayBuffer> ArenaSlotBase::GetArrayBuffer( |
| v8::Isolate* isolate) const { |
| DCHECK_NOT_NULL(page); |
| auto* header = static_cast<detail::ArenaPageHeader*>(page); |
| return header->buffer.Get(isolate); |
| } |
| |
| inline v8::Local<v8::DataView> ArenaSlotBase::GetPageDataView( |
| v8::Isolate* isolate) const { |
| DCHECK_NOT_NULL(page); |
| auto* header = static_cast<detail::ArenaPageHeader*>(page); |
| return header->GetDataView(isolate); |
| } |
| |
| inline v8::Local<v8::BigUint64Array> ArenaSlotBase::GetPageBigUint64Array( |
| v8::Isolate* isolate) const { |
| DCHECK_NOT_NULL(page); |
| auto* header = static_cast<detail::ArenaPageHeader*>(page); |
| return header->GetBigUint64Array(isolate); |
| } |
| |
| } // namespace node |
| |
| #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
| |
| #endif // SRC_ALIASED_STRUCT_H_ |