diff options
Diffstat (limited to 'source')
| -rw-r--r-- | source/core/slang-blob-builder.cpp | 473 | ||||
| -rw-r--r-- | source/core/slang-blob-builder.h | 352 | ||||
| -rw-r--r-- | source/core/slang-internally-linked-list.cpp | 2 | ||||
| -rw-r--r-- | source/core/slang-internally-linked-list.h | 126 | ||||
| -rw-r--r-- | source/core/slang-memory-arena.h | 11 | ||||
| -rw-r--r-- | source/core/slang-relative-ptr.cpp | 2 | ||||
| -rw-r--r-- | source/core/slang-relative-ptr.h | 97 | ||||
| -rw-r--r-- | source/core/slang-riff.h | 68 | ||||
| -rw-r--r-- | source/slang/slang-fossil.cpp | 187 | ||||
| -rw-r--r-- | source/slang/slang-fossil.h | 488 | ||||
| -rw-r--r-- | source/slang/slang-serialize-ast.cpp | 48 | ||||
| -rw-r--r-- | source/slang/slang-serialize-fossil.cpp | 1636 | ||||
| -rw-r--r-- | source/slang/slang-serialize-fossil.h | 741 | ||||
| -rw-r--r-- | source/slang/slang-serialize-riff.cpp | 40 | ||||
| -rw-r--r-- | source/slang/slang-serialize-riff.h | 12 | ||||
| -rw-r--r-- | source/slang/slang-serialize.h | 43 |
16 files changed, 4231 insertions, 95 deletions
diff --git a/source/core/slang-blob-builder.cpp b/source/core/slang-blob-builder.cpp new file mode 100644 index 000000000..10dd4003e --- /dev/null +++ b/source/core/slang-blob-builder.cpp @@ -0,0 +1,473 @@ +// slang-blob-builder.cpp +#include "slang-blob-builder.h" + +namespace Slang +{ + +// +// BlobBuilder +// + +BlobBuilder::BlobBuilder() + : _arena(4096) +{ +} + +void BlobBuilder::writeTo(Stream* stream) +{ + // First we scan through all the chunks to set their + // absolute offsets, which enables us to compute the + // correct values for relative pointers when we write + // them out. + // + SLANG_MAYBE_UNUSED + Size sizeComputed = _calcSizeAndSetCachedChunkOffsets(); + + // Now we can scan through the chunks again and write + // the bytes of each of their shards. + // + SLANG_MAYBE_UNUSED + Size sizeWritten = _writeChunksTo(stream); + + SLANG_ASSERT(sizeComputed == sizeWritten); +} + +Size BlobBuilder::_calcSizeAndSetCachedChunkOffsets() +{ + Size totalSize = 0; + for (auto chunk : _chunks) + { + auto chunkPrefixSize = chunk->getPrefixSize(); + auto chunkContentSize = chunk->getContentSize(); + auto chunkAlignment = chunk->getAlignment(); + + // We add the size of the chunk prefix (if any) *before* + // aligning the current offset. Doing it this way + // means that sometimes the prefix can fit "for free" + // in space that would otherwise be padding. + // + // For example, if the current `totalSize` is 4, the + // `chunkAlignment` is 16, and the `chunkPrefixSize` is 8, + // then the sequence will be: + // + // * Add `totalSize += chunkPrefixSize`, resulting in a `totalSize` + // of 12. + // + // * Round the `totalSize` up to the `chunkAlignment`, to compute + // a `chunkOffset` of 16. + // + // The result is that the chunk's content starts at offset 16, + // and the prefix can occupy the 8 bytes before that (so the + // prefix is at offset 8). + // + // In the best case this approach can save a few bytes here or + // there when `chunkAlignment` is larger than the `chunkPrefixSize`, + // and in the worst case it does no harm. + + totalSize += chunkPrefixSize; + auto chunkOffset = roundUpToAlignment(totalSize, chunkAlignment); + + chunk->_setCachedOffset(chunkOffset); + + totalSize = chunkOffset + chunkContentSize; + } + return totalSize; +} + +Size BlobBuilder::_writeChunksTo(Stream* stream) +{ + Size totalSize = 0; + for (auto chunk : _chunks) + { + auto chunkPrefixSize = chunk->getPrefixSize(); + auto chunkContentSize = chunk->getContentSize(); + auto chunkOffset = chunk->_getCachedOffset(); + + SLANG_ASSERT( + chunkOffset == roundUpToAlignment(totalSize + chunkPrefixSize, chunk->getAlignment())); + + auto paddingSize = chunkOffset - totalSize; + + // The "padding" before the chunk's content also + // includes the space reserved for the chunk's prefix. + // + // We can thus subtract the prefix size from the number + // of pad bytes to write. + + SLANG_ASSERT(paddingSize >= chunkPrefixSize); + paddingSize -= chunkPrefixSize; + + while (paddingSize--) + { + Byte padding = 0; + stream->write(&padding, sizeof(padding)); + } + + // The `ChunkBuilder::_writeTo()` call will write the + // prefix *and* the chunk content, so it is appropriate + // to call it here even when the total number of bytes + // written to the stream is not equal to the `chunkOffset` + // (because in that case the total bytes written so far + // should be `chunkOffset - chunkPrefixSize`). + // + chunk->_writeTo(stream); + + totalSize = chunkOffset + chunkContentSize; + } + + return totalSize; +} + +void BlobBuilder::writeToBlob(ISlangBlob** outBlob) +{ + OwnedMemoryStream stream(FileAccess::Write); + writeTo(&stream); + + List<uint8_t> data; + stream.swapContents(data); + + *outBlob = ListBlob::moveCreate(data).detach(); +} + +ChunkBuilder* BlobBuilder::createUnparentedChunk() +{ + auto chunk = new (_arena) ChunkBuilder(this); + return chunk; +} + +void BlobBuilder::addChunk(ChunkBuilder* chunk) +{ + // TODO(tfoley): it would be good to have a way to assert + // that the chunk has not already been added. + + _chunks.add(chunk); +} + +ChunkBuilder* BlobBuilder::addChunk() +{ + auto chunk = new (_arena) ChunkBuilder(this); + _chunks.add(chunk); + return chunk; +} + +ChunkBuilder* BlobBuilder::addChunkAfter(ChunkBuilder* existingChunk) +{ + auto newChunk = new (_arena) ChunkBuilder(this); + _chunks.insertAfter(existingChunk, newChunk); + return newChunk; +} + +// +// ChunkBuilder +// + +void ChunkBuilder::setAlignmentToAtLeast(Size alignment) +{ + SLANG_ASSERT(alignment > 0); + SLANG_ASSERT(isPowerOfTwo(alignment)); + + _contentAlignment = std::max(_contentAlignment, alignment); +} + +void ChunkBuilder::writePaddingToAlignTo(Size alignment) +{ + setAlignmentToAtLeast(alignment); + + auto alignedSize = roundUpToAlignment(_contentSize, alignment); + + auto requiredPaddingSize = alignedSize - _contentSize; + + while (requiredPaddingSize) + { + Byte padByte = 0; + writeData(&padByte, sizeof(padByte)); + requiredPaddingSize -= sizeof(padByte); + } +} + +ChunkBuilder::ChunkBuilder(BlobBuilder* parentBlob) + : _parentBlob(parentBlob) +{ +} + +Size ChunkBuilder::getContentSize() const +{ + return _contentSize; +} + +Size ChunkBuilder::getPrefixSize() const +{ + if (!_prefixShard) + return 0; + + return _prefixShard->getSize(); +} + +Size ChunkBuilder::getAlignment() const +{ + return _contentAlignment; +} + + +void ChunkBuilder::writeData(void const* data, Size size) +{ + // Adding no data should be a no-op. + // + if (size == 0) + return; + + + // The most interesting implementation detail here + // is that we will try to detect cases where we + // can re-use an existing `ShardBuilder` by adding the data + // to the end of that shard's allocation. + // + // This is only possible because of the way that + // we are using a single `MemoryArena` to allocate + // everything, which makes it possible that the + // next address the arena would return for an allocation + // of `size` bytes is the same as the ending address + // of the payload for the last shard of this chunk. + // + auto& arena = getParentBlob()->_getArena(); + + // We start by checking if this chunk already has + // a last shard that we could consider appending to. + // + auto lastShard = _childShards.getLast(); + if (lastShard && lastShard->_kind == ShardBuilder::Kind::Data) + { + // If there is a last shard, then we can compute + // the end address of its payload, and see if + // it is the same as the cursor of the arena + // we are allocating from. + // + auto payload = lastShard->_data.ptr; + auto payloadSize = lastShard->_size; + auto payloadEnd = (Byte*)payload + payloadSize; + if (payloadEnd == arena.getCursor()) + { + // Now that we've confirmed that the shard's + // payload ends at an address the arena could + // conceivably allocate from, we need to ask + // the arena to allocate `size` bytes from + // the current block it is using, and see if + // doing so succeeds. + // + if (arena.allocateCurrentUnaligned(size)) + { + // At this point, we've confirmed that we + // are in our special case, and the relevant + // bytes have been allocated from the arena. + // + // Now we can simply write the new data at + // what used to be the end address for the + // shard's payload, and adjust its state + // to account for the new allocation. + // + ::memcpy(payloadEnd, data, size); + + lastShard->_size = payloadSize + size; + _contentSize += size; + return; + } + } + } + + // If the special case above doesn't apply, we simply + // allocate a new shard to hold the data that was + // passed in. + // + auto shard = _createDataShard(data, size); + _childShards.add(shard); + _contentSize += size; +} + +void ChunkBuilder::addContentsOf(ChunkBuilder* otherChunk) +{ + auto otherPrefixShard = otherChunk->_prefixShard; + auto otherChunkSize = otherChunk->getContentSize(); + auto otherChunkAlignment = otherChunk->getAlignment(); + auto otherChunkShards = otherChunk->_childShards; + + otherChunk->_prefixShard = nullptr; + otherChunk->_contentSize = 0; + otherChunk->_contentAlignment = 1; + otherChunk->_childShards = InternallyLinkedList<ShardBuilder>(); + + if (otherPrefixShard) + { + // If the other chunk included a prefix, then + // it only makes sense to append it in the case + // where *this* chunk is completely empty. + + SLANG_ASSERT(!_prefixShard); + SLANG_ASSERT(!_childShards.getFirst()); + _prefixShard = otherPrefixShard; + } + + writePaddingToAlignTo(otherChunkAlignment); + _childShards.append(otherChunkShards); + _contentSize += otherChunkSize; +} + +ChunkBuilder* ChunkBuilder::addChunkAfter() +{ + return getParentBlob()->addChunkAfter(this); +} + +void ChunkBuilder::_writeRelativePtr(ChunkBuilder* targetChunk, Size ptrSize) +{ + SLANG_ASSERT(ptrSize != 0); + SLANG_ASSERT(ptrSize <= sizeof(UInt64)); + + writePaddingToAlignTo(ptrSize); + + if (!targetChunk) + { + UInt64 value = 0; + writeData(&value, ptrSize); + return; + } + + auto shard = _createRelativePtrShard(targetChunk, ptrSize); + _childShards.add(shard); + _contentSize += ptrSize; +} + +void ChunkBuilder::_addPrefixRelativePtr(ChunkBuilder* targetChunk, Size ptrSize) +{ + SLANG_ASSERT(targetChunk != nullptr); + SLANG_ASSERT(ptrSize != 0); + + SLANG_ASSERT(!_prefixShard); + + setAlignmentToAtLeast(ptrSize); + + auto shard = _createRelativePtrShard(targetChunk, ptrSize); + _prefixShard = shard; +} + +void ChunkBuilder::addPrefixData(void const* data, Size size) +{ + SLANG_ASSERT(data != nullptr); + SLANG_ASSERT(size != 0); + + SLANG_ASSERT(!_prefixShard); + + setAlignmentToAtLeast(size); + + auto shard = _createDataShard(data, size); + _prefixShard = shard; +} + +ShardBuilder* ChunkBuilder::_createDataShard(void const* data, Size size) +{ + auto& arena = getParentBlob()->_getArena(); + auto shard = new (arena) ShardBuilder(ShardBuilder::Kind::Data); + + auto shardData = arena.allocateUnaligned(size); + ::memcpy(shardData, data, size); + + shard->_data.ptr = shardData; + shard->_size = size; + + return shard; +} + +ShardBuilder* ChunkBuilder::_createRelativePtrShard(ChunkBuilder* targetChunk, Size ptrSize) +{ + auto& arena = getParentBlob()->_getArena(); + auto shard = new (arena) ShardBuilder(ShardBuilder::Kind::RelativePtr); + + shard->_relativePtr.targetChunk = targetChunk; + shard->_size = ptrSize; + + return shard; +} + +void ChunkBuilder::_writeTo(Stream* stream) +{ + auto chunkOffset = _getCachedOffset(); + + if (_prefixShard) + { + // Note that the prefix is written *before* the + // starting offset of the chunk's content, so we + // compute the appropriate offset to pass down. + // + auto prefixSize = _prefixShard->getSize(); + auto prefixOffset = chunkOffset - prefixSize; + + _prefixShard->_writeTo(stream, prefixOffset); + } + + auto shardOffset = chunkOffset; + for (auto shard : _childShards) + { + shard->_writeTo(stream, shardOffset); + shardOffset += shard->getSize(); + } + SLANG_ASSERT(shardOffset == chunkOffset + getContentSize()); +} + + +// +// ShardBuilder +// + +ShardBuilder::ShardBuilder(Kind kind) + : _kind(kind) +{ +} + +void ShardBuilder::_writeTo(Stream* stream, Size inSelfOffset) +{ + switch (_kind) + { + case Kind::Data: + { + stream->write(_data.ptr, _size); + } + break; + + case Kind::RelativePtr: + { + auto targetChunk = _relativePtr.targetChunk; + SLANG_ASSERT(targetChunk); + + auto selfOffset = intptr_t(inSelfOffset); + auto targetOffset = intptr_t(targetChunk->_getCachedOffset()); + + intptr_t relativeOffset = targetOffset - selfOffset; + + switch (_size) + { + case sizeof(Int32): + { + auto value = Int32(relativeOffset); + stream->write(&value, sizeof(value)); + } + break; + + case sizeof(Int64): + { + auto value = Int64(relativeOffset); + stream->write(&value, sizeof(value)); + } + break; + + default: + SLANG_UNEXPECTED("unsupported relative pointer size"); + break; + } + } + break; + + default: + SLANG_UNEXPECTED("unknown Fossil::ShardBuilder::Kind"); + break; + } +} + +} // namespace Slang diff --git a/source/core/slang-blob-builder.h b/source/core/slang-blob-builder.h new file mode 100644 index 000000000..0b59ee7ab --- /dev/null +++ b/source/core/slang-blob-builder.h @@ -0,0 +1,352 @@ +// slang-blob-builder.h +#ifndef SLANG_BLOB_BUILDER_H +#define SLANG_BLOB_BUILDER_H + +// This file provides utilities for building "blobs" of data +// where, for purposes, a blob is a contiguous sequence of +// bytes where the interpretation *of* those bytes depends +// only on the bytes themselves, and not other factors like +// the in-memory address of the blob, or the address/contents +// of memory not in the blob. +// +// Superficially, the task seems simple: just maintain a +// dynamically-sized array of bytes and append to it until +// you're done. If that's what you need, you're probably +// better off just using an `OwnedMemoryStream`. +// +// The utilities in this file deal with the case where you +// want to build some kind of offset-based data structure, +// so that parts of the blob will store byte offsets to +// other parts, while also being able to build parts of +// that structure "out of order", so that the final offset +// of a particular piece of data in the blob may not be +// known until everything *before* it has been fully built. + +#include "slang-basic.h" +#include "slang-internally-linked-list.h" +#include "slang-io.h" +#include "slang-memory-arena.h" + +namespace Slang +{ + +inline constexpr bool isPowerOfTwo(Size value) +{ + return value > 0 && (value - 1 & value) == 0; +} + +inline constexpr Size roundUpToAlignment(Size size, Size alignment) +{ + SLANG_ASSERT(isPowerOfTwo(alignment)); + + auto alignmentMask = Size(alignment) - 1; + return (size + alignmentMask) & ~alignmentMask; +} + +class ShardBuilder; +class ChunkBuilder; +struct BlobBuilder; + + +/// A utility type for composing a binary blob. +/// +/// A blob builder allows a blob to be composed as a sequence of discrete +/// chunks, allowing chunks to be added and written to in any order. +/// +/// Chunks can contain (relative) pointers to one another, with the correct +/// relative offsets being computed as part of writing the entire blob out. +/// +struct BlobBuilder +{ +public: + /// Construct an empty blob builder. + BlobBuilder(); + + /// Write the contents of the blob to the given `stream`. + void writeTo(Stream* stream); + + /// Create a copy of the contents of the blob and assign to `outBlob`. + void writeToBlob(ISlangBlob** outBlob); + + /// Add a new empty chunk to the end of the blob. + ChunkBuilder* addChunk(); + + /// Add a new empty chunk after the given `chunk`. + ChunkBuilder* addChunkAfter(ChunkBuilder* chunk); + + /// Create a chunk that is not initially part of the blob. + /// + /// The contents of the returned chunk will only become + /// part of the full blob if `addChunk()` is called later, + /// *or* if the contents of the new chunk are moved into + /// another chunk that gets added. + /// + ChunkBuilder* createUnparentedChunk(); + + /// Add a chunk to the blob that was initially not part of the blob. + void addChunk(ChunkBuilder* chunk); + +private: + InternallyLinkedList<ChunkBuilder> _chunks; + MemoryArena _arena; + + friend class ChunkBuilder; + friend class ShardBuilder; + MemoryArena& _getArena() { return _arena; } + + Size _calcSizeAndSetCachedChunkOffsets(); + Size _writeChunksTo(Stream* stream); +}; + +/// A chunk is a logically contiguous unit, such that data can +/// only ever be appended to it. As a result, offsets that are +/// relative to the start of a chunk are meaningful, and can be +/// used to encode relative offsets for pointers. +/// +/// Every `ChunkBuilder` is owned by its parent `BlobBuilder`. +/// A pointer to a `ChunkBuilder` will only be valid during the +/// lifetime of the parent `BlobBuilder`. +/// +/// Conceptually, a `ChunkBuilder` has the following: +/// +/// * A minimum *alignment* in bytes (initially 1) +/// +/// * Zero or more bytes of *content* data (initially empty) +/// +/// * An optional *prefix* consisting of zero or more bytes (initially absent) +/// +/// The content may be a mix of raw data (e.g., added via `writeData()`), +/// relative pointers to other chunks (`writeRelativePtr()`) and padding +/// bytes (`writePaddingToAlignTo()`). +/// +/// The prefix may only be either a single relative pointer, or a +/// single range of raw data bytes. +/// +/// When the parent blob written out as a flat buffer of bytes, the +/// following are guaranteed: +/// +/// * The content of the chunk will be a contiguous range of bytes +/// starting at some offset, and will not overlap any other chunks. +/// +/// * The byte offset where the chunk contents start will be a multiple +/// of the chunk's minimum alignment. +/// +/// * The bytes of the prefix (if any) will immediately precede the +/// bytes of the content. +/// +class ChunkBuilder : public InternallyLinkedList<ChunkBuilder>::Node +{ +public: + /// Get the blob builder that this chunk belongs to. + BlobBuilder* getParentBlob() const { return _parentBlob; } + + /// Get the required alignment of this chunk. + /// + /// The minimum alignment for a chunk starts at 1, + /// and may be increased by operations such as + /// `setAlignmentToAtLeast()` and `writePaddingToAlinTo()`. + /// + Size getAlignment() const; + + /// Potentially increases the required alignment of this chunk. + /// + /// If the alignment of the chunk is less than `alignment`, + /// then it will be increased to `alignment`. + /// + /// The `alignment` passed in must be a power of two. + /// + void setAlignmentToAtLeast(Size alignment); + + /// Get the size in bytes of the content of this chunk. + /// + /// Note that the size is not necessarily a multiple of the + /// alignment of the chunk. + /// + Size getContentSize() const; + + /// Write data into the chunk. + /// + /// The chunk will retain a copy of the data passed in, + /// so the `data` pointer only needs to be valid for + /// the duration of the call. + /// + /// Note that this operation does *not* adjust the + /// alignment of the chunk in any way. + /// + /// The data must only contain types that can be copied + /// bit-for-bit, and that do not depend on addresses + /// in meory. In particular no pointers (absolute or + /// relative) should be written. + /// + void writeData(void const* data, Size size); + + /// Append padding bytes to this chunk until its content size + /// is a multiple of `alignment`. + /// + /// May also increase the alignment of the chunk, as if + /// calling `setAlignmentToAtLeast(alignment)`. + /// + /// The padding bytes will all be zero. + /// + void writePaddingToAlignTo(Size alignment); + + /// Write a relative pointer to the given `targetChunk`. + /// + /// The type parameter `T` is used to determine the size + /// of the relative pointer (should be either 4 or 8 bytes). + /// + /// The bytes that eventually get written will contain + /// the computed offset of `targetChunk` minus the computed + /// offset of the first byte of the relative pointer itself. + /// + /// Acts as is `writePaddingToAlignTo(sizeof(T))` were + /// called immediately before. + /// + template<typename T> + void writeRelativePtr(ChunkBuilder* targetChunk) + { + _writeRelativePtr(targetChunk, sizeof(T)); + } + + /// Append the contents of another chunk to this one. + /// + /// This *moves* all of the contents of `chunk` into `this`. + /// After the operation completes `chunk` will be an empty + /// chunk with one-byte alignment. + /// + /// This operation is useful when accumulating data that + /// needs to be appended to the chunk, but where the correct + /// alignment for that data is not yet known; a use can + /// effectively create a temporary "sub-chunk" and then append + /// it to the main chunk once its correct alignment is known. + /// + void addContentsOf(ChunkBuilder* chunk); + + /// Get the size in bytes of the prefix of this chunk, if any. + /// + /// The prefix will be written so that the *end* offset of the + /// prefix data is the same as the starting offset of the + /// chunk's content. + /// + Size getPrefixSize() const; + + /// Add a prefix to this chunk, consisting of raw data. + /// + /// This chunk must not already have a prefix. + /// + void addPrefixData(void const* data, Size size); + + /// Add a prefix to this chunk, consisting of a relative pointer. + /// + /// This chunk must not already have a prefix. + /// + /// Updates the alignment of the chunk as if making a call to + /// `setAlignmentToAtLeast(sizeof(T))`. + /// + template<typename T> + void addPrefixRelativePtr(ChunkBuilder* targetChunk) + { + _addPrefixRelativePtr(targetChunk, sizeof(T)); + } + + /// Insert a new chunk into the blob, immediately after this chunk. + /// + ChunkBuilder* addChunkAfter(); + +private: + ChunkBuilder() = delete; + ChunkBuilder(ChunkBuilder const&) = delete; + ChunkBuilder(ChunkBuilder&&) = delete; + + friend struct BlobBuilder; + friend class ShardBuilder; + + ChunkBuilder(BlobBuilder* parentBlob); + + Size _contentSize = 0; + Size _contentAlignment = 1; + + InternallyLinkedList<ShardBuilder> _childShards; + BlobBuilder* _parentBlob = nullptr; + + Size _cachedOffset = ~Size(0); + Size _getCachedOffset() { return _cachedOffset; } + void _setCachedOffset(Size offset) { _cachedOffset = offset; } + + void _writeRelativePtr(ChunkBuilder* targetChunk, Size ptrSize); + + ShardBuilder* _createDataShard(void const* data, Size size); + ShardBuilder* _createRelativePtrShard(ChunkBuilder* targetChunk, Size ptrSize); + + ShardBuilder* _prefixShard = nullptr; + + void _writeTo(Stream* stream); + + void _addPrefixRelativePtr(ChunkBuilder* targetChunk, Size ptrSize); +}; + +/// A shard is a unit of contiguously-allocated data that makes +/// up part of a chunk. +/// +/// Shards are *not* meant to be directly manipulated by users; +/// they are an implementation detail of `ChunkBuilder`. +/// +/// Every `ShardBuilder` is owned by its parent `ChunkBuilder`. +/// A pointer to a `ShardBuilder` will only be valid during the +/// lifetime of the parent `ChunkBuilder`. +/// +class ShardBuilder : public InternallyLinkedList<ShardBuilder>::Node +{ +public: + // There are two kinds of shards that may appear in a chunk: + // + // * Shards that hold plain data that will be part of the + // serialized chunk. + // + // * Shards that represent a relative pointer to some chunk, + // which cannot have their exact binary value determined + // until the offsets of chunk/shards have been finalized. + + enum class Kind + { + Data, + RelativePtr, + }; + + Size getSize() const { return _size; } + +private: + ShardBuilder() = delete; + ShardBuilder(ShardBuilder const&) = delete; + ShardBuilder(ShardBuilder&&) = delete; + + friend class ChunkBuilder; + ShardBuilder(Kind kind); + + void _writeTo(Stream* stream, Size selfOffset); + + /// Kind of this shard (data or relative pointer) + Kind _kind = Kind::Data; + + union + { + /// Used when `_kind == Kind::Data` + struct + { + void const* ptr; + } _data; + + /// Used when `_kind == Kind::RelativePtr` + struct + { + ChunkBuilder* targetChunk; + } _relativePtr; + }; + + // Size of this shard in bytes. + Size _size = 0; +}; + +} // namespace Slang + +#endif diff --git a/source/core/slang-internally-linked-list.cpp b/source/core/slang-internally-linked-list.cpp new file mode 100644 index 000000000..27bbd77b0 --- /dev/null +++ b/source/core/slang-internally-linked-list.cpp @@ -0,0 +1,2 @@ +// slang-internally-linked-list.cpp +#include "slang-internally-linked-list.h" diff --git a/source/core/slang-internally-linked-list.h b/source/core/slang-internally-linked-list.h new file mode 100644 index 000000000..168885531 --- /dev/null +++ b/source/core/slang-internally-linked-list.h @@ -0,0 +1,126 @@ +// slang-internally-linked-list.h +#ifndef SLANG_INTERNALLY_LINKED_LIST_H +#define SLANG_INTERNALLY_LINKED_LIST_H + +// This file provides support for the idiom of a linked +// list of values where the "next" pointer is stored in +// the values themselves (thus requiring no additional +// allocation for list nodes, at the price of any given +// value only being able to appear in a single list). + +#include "slang-basic.h" + +namespace Slang +{ + +/// A linked list where the elements are themselves the nodes. +/// +/// The type parameter `T` should be a type that publicly +/// inherits from `InternallyLinkedList<T>::Node`. +/// +template<typename T> +struct InternallyLinkedList +{ +public: + struct Node + { + public: + Node() {} + + private: + friend struct InternallyLinkedList<T>; + T* _next = nullptr; + }; + + struct Iterator + { + public: + Iterator() {} + + Iterator(T* node) + : _node(node) + { + } + + T* operator*() const { return _node; } + + void operator++() { _node = static_cast<Node const*>(_node)->_next; } + + bool operator!=(Iterator const& that) const { return _node != that._node; } + + private: + T* _node = nullptr; + }; + + Iterator begin() { return Iterator(_first); } + + Iterator end() { return Iterator(); } + + T* getFirst() const { return _first; } + + T* getLast() const { return _last; } + + void add(T* element) + { + SLANG_ASSERT(element != nullptr); + if (!_last) + { + SLANG_ASSERT(_first == nullptr); + + _first = element; + _last = element; + } + else + { + SLANG_ASSERT(_first != nullptr); + + _last->_next = element; + _last = element; + } + } + + void insertAfter(T* existingElement, T* newElement) + { + SLANG_ASSERT(existingElement != nullptr); + SLANG_ASSERT(newElement != nullptr); + if (existingElement == _last) + { + add(newElement); + } + else + { + newElement->_next = existingElement->_next; + existingElement->_next = newElement; + } + } + + void append(InternallyLinkedList<T> const& other) + { + if (!other._first) + { + } + else if (!_last) + { + _first = other._first; + _last = other._last; + } + else + { + SLANG_ASSERT(_first != nullptr); + + _last->_next = other._first; + _last = other._last; + } + } + +private: + T* _first = nullptr; + T* _last = nullptr; +}; + +template<typename T> +using InternallyLinkedListNode = InternallyLinkedList<T>::Node; + +} // namespace Slang + +#endif diff --git a/source/core/slang-memory-arena.h b/source/core/slang-memory-arena.h index 03631befe..714918b9a 100644 --- a/source/core/slang-memory-arena.h +++ b/source/core/slang-memory-arena.h @@ -476,4 +476,15 @@ SLANG_FORCE_INLINE void operator delete(void* memory, Slang::MemoryArena& arena) SLANG_UNUSED(arena); } +SLANG_FORCE_INLINE void* operator new[](size_t size, Slang::MemoryArena& arena) +{ + return arena.allocate(size); +} + +SLANG_FORCE_INLINE void operator delete[](void* memory, Slang::MemoryArena& arena) +{ + SLANG_UNUSED(memory); + SLANG_UNUSED(arena); +} + #endif // SLANG_MEMORY_ARENA_H diff --git a/source/core/slang-relative-ptr.cpp b/source/core/slang-relative-ptr.cpp new file mode 100644 index 000000000..0c4da2c6e --- /dev/null +++ b/source/core/slang-relative-ptr.cpp @@ -0,0 +1,2 @@ +// slang-relative-ptr.cpp +#include "slang-relative-ptr.h" diff --git a/source/core/slang-relative-ptr.h b/source/core/slang-relative-ptr.h new file mode 100644 index 000000000..6c4bc942c --- /dev/null +++ b/source/core/slang-relative-ptr.h @@ -0,0 +1,97 @@ +// slang-relative-ptr.h +#ifndef SLANG_RELATIVE_PTR_H +#define SLANG_RELATIVE_PTR_H + +// This file implements a smart pointer type `RelativePtr<T>` +// that, rather than storing the actual *address* of a value +// of type `T`, stores the relative offset (in bytes) between +// the target `T*` and the address of the `RelativePtr<T>` +// itself. +// +// This kind of pointer representation can be useful when +// implementing "memory-mappable" data structures that can +// still conveniently represent complicated object graphs. + +#include "slang-basic.h" + +namespace Slang +{ +namespace detail +{ +struct RelativePtr32Traits +{ + using Offset = Int32; + using UOffset = UInt32; +}; + +struct RelativePtr64Traits +{ + using Offset = Int64; + using UOffset = UInt64; +}; +} // namespace detail + +template<typename T, typename Traits> +struct RelativePtr +{ +public: + using This = RelativePtr<T, Traits>; + using Value = T; + using RawPtr = T*; + using Offset = typename Traits::Offset; + using UOffset = typename Traits::UOffset; + + RelativePtr() = default; + RelativePtr(RelativePtr const& ptr) { set(ptr); } + RelativePtr(RelativePtr&& ptr) { set(ptr); } + RelativePtr(T* ptr) { set(ptr); } + + void operator=(RelativePtr const& ptr) { set(ptr); } + void operator=(RelativePtr&& ptr) { set(ptr); } + void operator=(T* ptr) { set(ptr); } + + T* get() const + { + if (_offset == 0) + { + return nullptr; + } + + intptr_t thisAddr = intptr_t(this); + intptr_t targetAddr = thisAddr + intptr_t(_offset); + + return (T*)(targetAddr); + } + + void set(T* ptr) + { + if (ptr == nullptr) + { + _offset = 0; + return; + } + + intptr_t thisAddr = intptr_t(this); + intptr_t targetAddr = intptr_t(ptr); + intptr_t offsetVal = targetAddr - thisAddr; + + _offset = Offset(offsetVal); + SLANG_ASSERT(intptr_t(_offset) == offsetVal); + } + + operator T*() const { return get(); } + T* operator->() const { return get(); } + +private: + Offset _offset = 0; +}; + +template<typename T> +using RelativePtr32 = RelativePtr<T, detail::RelativePtr32Traits>; + +template<typename T> +using RelativePtr64 = RelativePtr<T, detail::RelativePtr64Traits>; + +} // namespace Slang + +#endif diff --git a/source/core/slang-riff.h b/source/core/slang-riff.h index 20b1d7c66..9459c8a55 100644 --- a/source/core/slang-riff.h +++ b/source/core/slang-riff.h @@ -16,6 +16,7 @@ // #include "slang-basic.h" +#include "slang-internally-linked-list.h" #include "slang-memory-arena.h" #include "slang-stream.h" #include "slang-writer.h" @@ -564,73 +565,6 @@ T const* as(Chunk const* chunk) return static_cast<T const*>(chunk); } -/// A linked list where the elements are themselves the nodes. -/// -template<typename T> -struct InternallyLinkedList -{ -public: - struct Node - { - public: - Node() {} - - private: - friend struct InternallyLinkedList<T>; - T* _next = nullptr; - }; - - struct Iterator - { - public: - Iterator() {} - - Iterator(T* node) - : _node(node) - { - } - - T* operator*() const { return _node; } - - void operator++() { _node = static_cast<Node const*>(_node)->_next; } - - bool operator!=(Iterator const& that) const { return _node != that._node; } - - private: - T* _node = nullptr; - }; - - Iterator begin() { return Iterator(_first); } - - Iterator end() { return Iterator(); } - - T* getFirst() const { return _first; } - - T* getLast() const { return _last; } - - void add(T* element) - { - if (!_last) - { - SLANG_ASSERT(_first == nullptr); - - _first = element; - _last = element; - } - else - { - SLANG_ASSERT(_first != nullptr); - - _last->_next = element; - _last = element; - } - } - -private: - T* _first = nullptr; - T* _last = nullptr; -}; - /// A builder for a chunk in a RIFF. class ChunkBuilder : public InternallyLinkedList<ChunkBuilder>::Node { diff --git a/source/slang/slang-fossil.cpp b/source/slang/slang-fossil.cpp new file mode 100644 index 000000000..e3b2e2c9d --- /dev/null +++ b/source/slang/slang-fossil.cpp @@ -0,0 +1,187 @@ +// slang-fossil.cpp +#include "slang-fossil.h" + +namespace Slang +{ +namespace Fossil +{ + +const char Fossil::Header::kMagic[16] = { + '\xAB', // byte 0 + 'f', // byte 1 + 'o', // byte 2 + 's', // byte 3 + 's', // byte 4 + 'i', // byte 5 + 'l', // byte 6 + ' ', // byte 7 + '1', // byte 8 + '0', // byte 9 + '0', // byte 10 + '\xBB', // byte 11 + '\r', // byte 12 + '\n', // byte 13 + '\x1A', // byte 14 + '\n' // byte 15 +}; + +FossilizedValRef getRootValue(ISlangBlob* blob) +{ + return getRootValue(blob->getBufferPointer(), blob->getBufferSize()); +} + +FossilizedValRef getRootValue(void const* data, Size size) +{ + if (!data) + { + SLANG_UNEXPECTED("bad format for fossil"); + } + + // There must be enough data to at least hold the header. + // + // (In practice there would need to be more data than + // just the header, but checking this invariant is a start). + // + if (size < sizeof(Fossil::Header)) + { + SLANG_UNEXPECTED("bad format for fossil"); + } + + // Once we've checked that there's enough data, we can read + // the contents of the header. + // + auto header = reinterpret_cast<Fossil::Header const*>(data); + + // The "magic" bytes at the start of the header must be + // what we expect (which is the contents of `Fossil::Header::kMagic`). + // + if (memcmp(header->magic, Fossil::Header::kMagic, sizeof(Fossil::Header::kMagic)) != 0) + { + SLANG_UNEXPECTED("bad format for fossil"); + } + + auto reportedSize = header->totalSizeIncludingHeader; + if (reportedSize > size) + { + SLANG_UNEXPECTED("bad format for fossil"); + } + + auto rootValueVariant = header->rootValue.get(); + if (!rootValueVariant) + { + SLANG_UNEXPECTED("bad format for fossil"); + } + + return FossilizedValRef( + rootValueVariant->getContentData(), + rootValueVariant->getContentLayout()); +} + +} // namespace Fossil + +Size FossilizedStringObj::getSize() const +{ + auto sizePtr = (FossilUInt*)this - 1; + return Size(*sizePtr); +} + +UnownedTerminatedStringSlice FossilizedStringObj::getValue() const +{ + auto size = getSize(); + return UnownedTerminatedStringSlice((char*)this, size); +} + +Count FossilizedContainerObj::getElementCount() const +{ + auto countPtr = (FossilUInt*)this - 1; + return Size(*countPtr); +} + +FossilizedValLayout* FossilizedVariantObj::getContentLayout() const +{ + auto layoutPtrPtr = (FossilizedPtr<FossilizedValLayout>*)this - 1; + return (*layoutPtrPtr).get(); +} + +FossilizedValRef getPtrTarget(FossilizedPtrValRef ptrRef) +{ + auto ptrLayout = ptrRef.getLayout(); + auto ptrPtr = ptrRef.getData(); + return FossilizedValRef(ptrPtr->getTargetData(), ptrLayout->elementLayout); +} + +bool hasValue(FossilizedOptionalObjRef optionalRef) +{ + return optionalRef.getData() != nullptr; +} + +FossilizedValRef getValue(FossilizedOptionalObjRef optionalRef) +{ + auto optionalLayout = optionalRef.getLayout(); + auto valuePtr = optionalRef.getData(); + return FossilizedValRef(valuePtr, optionalLayout->elementLayout); +} + +Count getElementCount(FossilizedContainerObjRef containerRef) +{ + if (!containerRef) + return 0; + + auto containerPtr = containerRef.getData(); + return containerPtr->getElementCount(); +} + +FossilizedValRef getElement(FossilizedContainerObjRef containerRef, Index index) +{ + SLANG_ASSERT(index >= 0); + SLANG_ASSERT(index < getElementCount(containerRef)); + + auto containerLayout = containerRef.getLayout(); + auto elementLayout = containerLayout->elementLayout.get(); + auto elementStride = containerLayout->elementStride; + + auto elementsPtr = (Byte*)containerRef.getData(); + auto elementPtr = (FossilizedVal*)(elementsPtr + elementStride * index); + return FossilizedValRef(elementPtr, elementLayout); +} + +Count getFieldCount(FossilizedRecordValRef recordRef) +{ + auto recordLayout = recordRef.getLayout(); + return recordLayout->fieldCount; +} + +FossilizedRecordElementLayout* FossilizedRecordLayout::getField(Index index) const +{ + SLANG_ASSERT(index >= 0); + SLANG_ASSERT(index < fieldCount); + + auto fieldsPtr = (FossilizedRecordElementLayout*)(this + 1); + return fieldsPtr + index; +} + + +FossilizedValRef getField(FossilizedRecordValRef recordRef, Index index) +{ + SLANG_ASSERT(index >= 0); + SLANG_ASSERT(index < getFieldCount(recordRef)); + + auto recordLayout = recordRef.getLayout(); + auto field = recordLayout->getField(index); + + auto fieldsPtr = (Byte*)recordRef.getData(); + auto fieldPtr = (FossilizedVal*)(fieldsPtr + field->offset); + return FossilizedValRef(fieldPtr, field->layout); +} + +FossilizedValRef getVariantContent(FossilizedVariantObjRef variantRef) +{ + return getVariantContent(variantRef.getData()); +} + +FossilizedValRef getVariantContent(FossilizedVariantObj* variantPtr) +{ + return FossilizedValRef(variantPtr->getContentData(), variantPtr->getContentLayout()); +} + +} // namespace Slang diff --git a/source/slang/slang-fossil.h b/source/slang/slang-fossil.h new file mode 100644 index 000000000..dcc12bacb --- /dev/null +++ b/source/slang/slang-fossil.h @@ -0,0 +1,488 @@ +// slang-fossil.h +#ifndef SLANG_FOSSIL_H +#define SLANG_FOSSIL_H + +// +// This file defines a memory-mappable binary format for +// serialized object graphs. To distinguish this specific +// format from other serialization formats used in the +// Slang project, we refer to these serialized objects +// as *fossilized* objects, and to the format as the +// *fossil format*. +// +// The term "fossil" is being used here to refer to formerly +// "live" objects that have been converted into an alternative +// form that can no longer perform their original functions, +// but that can still be inspected and dug through. +// + +#include "../core/slang-relative-ptr.h" + +namespace Slang +{ +// A key part of the fossil representation is the use of *relative pointers*, +// so that a fossilized object graph can be traversed dirctly in memory +// without having to deserialize any of the intermediate objects. +// +// Fossil uses 32-bit relative pointers, to keep the format compact. + +template<typename T> +using FossilizedPtr = RelativePtr32<T>; + +// Various other parts of the format need to store offsets or counts, +// and for consistency we will store them with the same number of +// bits as the relative pointers already used in the format. +// +// To make it easier for us to (potentially) change the relative +// pointer size down the line, we define type aliases for the +// general-purpose integer types that will be used in fossilized data. + +using FossilInt = FossilizedPtr<void>::Offset; +using FossilUInt = FossilizedPtr<void>::UOffset; + +// +// The fossil format supports data that is *self-describing*. +// +// A `FossilizedValLayout` describes the in-memory layout of a fossilized +// value. Given a `FossilizedValLayout` and a pointer to the data +// for a particular value, it is possible to inspect the structure +// of the fossilized data. +// +// If all you have is a `FossilizedVal*`, then there is no way to access +// its contents without assuming it is of some particular type and casting +// it. +// +// A `FossilizedVariantObj` is a fossilized value that is self-describing; +// it stores a (relative) pointer to a layout, which can be used to inspect +// its own data/state. +// + +struct FossilizedVal; +struct FossilizedValLayout; +struct FossilizedVariantObj; + +/// Kinds of values that can appear in fossilized data. +enum class FossilizedValKind : FossilUInt +{ + Bool, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float32, + Float64, + String, + Array, + Optional, + Dictionary, + Tuple, + Struct, + Ptr, + Variant, +}; + +/// Layout information about a fossilized value in memory. +/// +/// +/// Every `FossilizedValLayout` stores the kind of the value. +/// Based on that kind, specific additional fields may be +/// available as part of the layout. +/// +struct FossilizedValLayout +{ + FossilizedValKind kind; +}; + +struct FossilizedPtrLikeLayout +{ + // Note: we aren't using inheritance in the definitions + // of these types, because per the letter of the law in + // C++, a type is only "standard layout" when there is + // only a single type in the inheritance hierarchy that + // has (non-static) data members. + + FossilizedValKind kind; + FossilizedPtr<FossilizedValLayout> elementLayout; +}; + +struct FossilizedContainerLayout +{ + FossilizedValKind kind; + FossilizedPtr<FossilizedValLayout> elementLayout; + FossilUInt elementStride; +}; + +struct FossilizedRecordElementLayout +{ + FossilizedPtr<FossilizedValLayout> layout; + FossilUInt offset; +}; + +struct FossilizedRecordLayout +{ + FossilizedValKind kind; + FossilUInt fieldCount; + + // FossilizedRecordElementLayout elements[]; + + FossilizedRecordElementLayout* getField(Index index) const; +}; + +/// A reference to a fossilized value in memory (of type T), and its layout. +/// +template<typename T> +struct FossilizedValRef_ +{ +public: + using Val = T; + using Layout = typename T::Layout; + + /// Construct a null reference. + /// + FossilizedValRef_() {} + + /// Construct a reference to the given `data`, assuming it has the given `layout`. + /// + FossilizedValRef_(T* data, Layout* layout) + : _data(data), _layout(layout) + { + } + + /// Get the kind of value being referenced. + /// + /// This reference must not be null. + /// + FossilizedValKind getKind() + { + SLANG_ASSERT(getLayout()); + return getLayout()->kind; + } + + /// Get the layout of the value being referenced. + /// + Layout* getLayout() { return _layout; } + + /// Get a pointer to the value being referenced. + /// + T* getData() { return _data; } + + operator T*() const { return _data; } + + T* operator->() { return _data; } + +private: + T* _data = nullptr; + Layout* _layout = nullptr; +}; + +using FossilizedValRef = FossilizedValRef_<FossilizedVal>; + +/// A fossilized value in memory. +/// +/// There isn't a lot that can be done with a bare pointer to +/// a `FossilizedVal`. This type is mostly declared to allow +/// us to make it explicit when a pointer points to a fossilized +/// value (even if we don't know anything about its layout). +/// +struct FossilizedVal +{ +public: + using Kind = FossilizedValKind; + using Layout = FossilizedValLayout; + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) + { + SLANG_UNUSED(kind); + return true; + } + +protected: + FossilizedVal() = default; + FossilizedVal(FossilizedVal const&) = default; + FossilizedVal(FossilizedVal&&) = default; + ~FossilizedVal() = default; +}; + +template<typename T, FossilizedValKind kKind> +struct FossilizedSimpleVal : FossilizedVal +{ +public: + T getValue() const { return _value; } + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) { return kind == kKind; } + +private: + T _value; +}; + +using FossilizedInt8Val = FossilizedSimpleVal<int8_t, FossilizedValKind::Int8>; +using FossilizedInt16Val = FossilizedSimpleVal<int16_t, FossilizedValKind::Int16>; +using FossilizedInt32Val = FossilizedSimpleVal<int32_t, FossilizedValKind::Int32>; +using FossilizedInt64Val = FossilizedSimpleVal<int64_t, FossilizedValKind::Int64>; + +using FossilizedUInt8Val = FossilizedSimpleVal<uint8_t, FossilizedValKind::UInt8>; +using FossilizedUInt16Val = FossilizedSimpleVal<uint16_t, FossilizedValKind::UInt16>; +using FossilizedUInt32Val = FossilizedSimpleVal<uint32_t, FossilizedValKind::UInt32>; +using FossilizedUInt64Val = FossilizedSimpleVal<uint64_t, FossilizedValKind::UInt64>; + +using FossilizedFloat32Val = FossilizedSimpleVal<float, FossilizedValKind::Float32>; +using FossilizedFloat64Val = FossilizedSimpleVal<double, FossilizedValKind::Float64>; + +struct FossilizedBoolVal : FossilizedVal +{ +public: + bool getValue() const { return _value != 0; } + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) { return kind == Kind::Bool; } + +private: + uint8_t _value; +}; + +struct FossilizedPtrVal : FossilizedVal +{ +public: + using Layout = FossilizedPtrLikeLayout; + + FossilizedVal* getTargetData() const { return _value.get(); } + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) { return kind == Kind::Ptr; } + +private: + FossilizedPtr<FossilizedVal> _value; +}; + + +struct FossilizedRecordVal : FossilizedVal +{ +public: + using Layout = FossilizedRecordLayout; + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) + { + switch (kind) + { + default: + return false; + + case Kind::Struct: + case Kind::Tuple: + return true; + } + } +}; + +// +// Some of the following subtypes of `FossilizedVal` are +// named as `Fossilized*Obj` rather than `Fossilized*Val`, +// to indicate that they will only ever be located on the +// other side of a pointer indirection. +// +// E.g., a field of a fossilized struct value should never +// have a layout claiming it to be of kind `String`; instead +// it should show as a field of kind `Ptr`, where the +// pointed-to type is `String`. The same goes for `Optional`, +// `Array`, and `Dictionary`. +// +// This distinction only matters when dealing with things like +// an *optional* string, because instead of an in-memory +// layout like `Ptr -> Optional -> Ptr -> String`, the fossilized +// data will simply store `Ptr -> Optional -> String`. +// + +struct FossilizedStringObj : FossilizedVal +{ +public: + Size getSize() const; + UnownedTerminatedStringSlice getValue() const; + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) { return kind == Kind::String; } + +private: + // Before the `this` address, there is a `FossilUInt` + // with the size of the string in bytes. + // + // At the `this` address there is a nul-terminated + // serquence of `getSize() + 1` bytes. +}; + +struct FossilizedOptionalObj : FossilizedVal +{ +public: + using Layout = FossilizedPtrLikeLayout; + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) { return kind == Kind::Optional; } + + FossilizedVal* getValue() { return this; } + + FossilizedVal const* getValue() const { return this; } + +private: + // An absent optional is encoded as a null pointer + // (so `this` would be null), while a present value + // is encoded as a pointer to that value. Thus the + // held value is at the same address as `this`. +}; + +struct FossilizedContainerObj : FossilizedVal +{ +public: + using Layout = FossilizedContainerLayout; + + Count getElementCount() const; + + /// Determine if a value with the given `kind` should be allowed to cast to this type. + static bool _isMatchingKind(Kind kind) + { + switch (kind) + { + default: + return false; + + case Kind::Array: + case Kind::Dictionary: + return true; + } + } + +private: + // Before the `this` address, there is a `FossilUInt` + // with the number of elements. + // + // At the `this` address there is a sequence of + // `getCount()` elements. The layout of those elements + // cannot be determined without having a `FossilizedContainerLayout` + // for this container. +}; + +struct FossilizedVariantObj : FossilizedVal +{ +public: + FossilizedValLayout* getContentLayout() const; + + + FossilizedVal* getContentData() { return this; } + FossilizedVal const* getContentData() const { return this; } + + static bool _isMatchingKind(Kind kind) { return kind == Kind::Variant; } + +private: + // Before the `this` address, there is a `FossilizedPtr<FossilizedValLayout>` + // with the layout of the content. + // + // The content itself starts at the `this` address, with its + // layout determined by `getContentLayout()`. +}; + +/// Dynamic cast of a reference to a fossilized value. +/// +template<typename T, typename U> +FossilizedValRef_<T> as(FossilizedValRef_<U> valRef) +{ + if (!valRef || !T::_isMatchingKind(valRef.getKind())) + return FossilizedValRef_<T>(); + + return FossilizedValRef_<T>( + static_cast<T*>(valRef.getData()), + reinterpret_cast<typename T::Layout*>(valRef.getLayout())); +} + +using FossilizedInt8ValRef = FossilizedValRef_<FossilizedInt8Val>; +using FossilizedInt16ValRef = FossilizedValRef_<FossilizedInt16Val>; +using FossilizedInt32ValRef = FossilizedValRef_<FossilizedInt32Val>; +using FossilizedInt64ValRef = FossilizedValRef_<FossilizedInt64Val>; +using FossilizedUInt8ValRef = FossilizedValRef_<FossilizedUInt8Val>; +using FossilizedUInt16ValRef = FossilizedValRef_<FossilizedUInt16Val>; +using FossilizedUInt32ValRef = FossilizedValRef_<FossilizedUInt32Val>; +using FossilizedUInt64ValRef = FossilizedValRef_<FossilizedUInt64Val>; +using FossilizedFloat32ValRef = FossilizedValRef_<FossilizedFloat32Val>; +using FossilizedFloat64ValRef = FossilizedValRef_<FossilizedFloat64Val>; +using FossilizedBoolValRef = FossilizedValRef_<FossilizedBoolVal>; +using FossilizedStringObjRef = FossilizedValRef_<FossilizedStringObj>; +using FossilizedPtrValRef = FossilizedValRef_<FossilizedPtrVal>; +using FossilizedOptionalObjRef = FossilizedValRef_<FossilizedOptionalObj>; +using FossilizedContainerObjRef = FossilizedValRef_<FossilizedContainerObj>; +using FossilizedRecordValRef = FossilizedValRef_<FossilizedRecordVal>; +using FossilizedVariantObjRef = FossilizedValRef_<FossilizedVariantObj>; + +FossilizedValRef getPtrTarget(FossilizedPtrValRef ptrRef); + +bool hasValue(FossilizedOptionalObjRef optionalRef); +FossilizedValRef getValue(FossilizedOptionalObjRef optionalRef); + +Count getElementCount(FossilizedContainerObjRef containerRef); +FossilizedValRef getElement(FossilizedContainerObjRef containerRef, Index index); + +Count getFieldCount(FossilizedRecordValRef recordRef); +FossilizedValRef getField(FossilizedRecordValRef recordRef, Index index); + +FossilizedValRef getVariantContent(FossilizedVariantObjRef variantRef); +FossilizedValRef getVariantContent(FossilizedVariantObj* variantPtr); + + +namespace Fossil +{ +using RelativePtrOffset = FossilizedPtr<void>::Offset; + +/// Header for a fossil-format file or blob. +/// +/// A blob of fossilized data must start with a `Header` +/// that is properly formatted. +/// +struct Header +{ + /// The "magic" bytes used to identify this is a fossil-format blob. + char magic[16]; + + /// The expected bytes that should appear in `magic` + /// + static const char kMagic[16]; + + /// The total size of the fossil-format blob, including this header. + UInt64 totalSizeIncludingHeader; + + /// Flags; reserved for future use. + UInt32 flags; + + /// A relative pointer to the root value of the object graph. + /// + /// A fossil-format blob may only have one root value, and that + /// value *must* be a variant (so that it can reference the + /// layout information that describes itself). The *content* + /// of the root object can be arbitrary, so applications may + /// store multiple values using an array, struct, etc. + /// + FossilizedPtr<FossilizedVariantObj> rootValue; +}; + +/// Get the root object from a fossilized blob. +/// +/// This operation performs some basic validation on the blob to +/// ensure that it doesn't seem incorrectly sized or otherwise +/// corrupted/malformed. +/// +FossilizedValRef getRootValue(ISlangBlob* blob); + +/// Get the root object from a fossilized blob. +/// +/// This operation performs some basic validation on the blob to +/// ensure that it doesn't seem incorrectly sized or otherwise +/// corrupted/malformed. +/// +FossilizedValRef getRootValue(void const* data, Size size); +} // namespace Fossil + +} // namespace Slang + +#endif diff --git a/source/slang/slang-serialize-ast.cpp b/source/slang/slang-serialize-ast.cpp index 3a2d3818e..02dd374c1 100644 --- a/source/slang/slang-serialize-ast.cpp +++ b/source/slang/slang-serialize-ast.cpp @@ -5,6 +5,7 @@ #include "slang-compiler.h" #include "slang-diagnostics.h" #include "slang-mangle.h" +#include "slang-serialize-fossil.h" #include "slang-serialize-riff.h" namespace Slang @@ -202,7 +203,7 @@ using ASTSerializer = Serializer_<ASTSerializerImpl>; template<typename T> void serializeObject(ASTSerializer const& serializer, T*& value, NodeBase*) { - SLANG_SCOPED_SERIALIZER_STRUCT(serializer); + SLANG_SCOPED_SERIALIZER_VARIANT(serializer); serializer->handleASTNode(*(NodeBase**)&value); } @@ -224,7 +225,7 @@ void serialize(ASTSerializer const& serializer, SourceLoc& value) void serialize(ASTSerializer const& serializer, RequirementWitness& value) { - SLANG_SCOPED_SERIALIZER_TAGGED_UNION(serializer); + SLANG_SCOPED_SERIALIZER_VARIANT(serializer); serialize(serializer, value.m_flavor); switch (value.m_flavor) { @@ -406,7 +407,7 @@ void serialize(ASTSerializer const& serializer, SPIRVAsmInst& value) void serialize(ASTSerializer const& serializer, ValNodeOperand& value) { - SLANG_SCOPED_SERIALIZER_TAGGED_UNION(serializer); + SLANG_SCOPED_SERIALIZER_VARIANT(serializer); serialize(serializer, value.kind); switch (value.kind) { @@ -480,19 +481,19 @@ struct ASTEncodingContext : ASTSerializerImpl { public: ASTEncodingContext( - RIFF::BuildCursor& cursor, + ISerializerImpl* writer, ModuleDecl* module, SerialSourceLocWriter* sourceLocWriter) - : _writer(cursor.getCurrentChunk()), _module(module), _sourceLocWriter(sourceLocWriter) + : _writer(writer), _module(module), _sourceLocWriter(sourceLocWriter) { } private: - RIFFSerialWriter _writer; + ISerializerImpl* _writer = nullptr; ModuleDecl* _module = nullptr; SerialSourceLocWriter* _sourceLocWriter = nullptr; - virtual ISerializerImpl* getBaseSerializer() override { return &_writer; } + virtual ISerializerImpl* getBaseSerializer() override { return _writer; } virtual void handleName(Name*& value) override; virtual void handleSourceLoc(SourceLoc& value) override; @@ -531,7 +532,7 @@ public: Linkage* linkage, ASTBuilder* astBuilder, DiagnosticSink* sink, - RIFF::Chunk const* baseChunk, + ISerializerImpl* reader, SerialSourceLocReader* sourceLocReader, SourceLoc requestingSourceLoc) : _linkage(linkage) @@ -539,7 +540,7 @@ public: , _sink(sink) , _sourceLocReader(sourceLocReader) , _requestingSourceLoc(requestingSourceLoc) - , _riffReader(baseChunk) + , _reader(reader) { } @@ -549,9 +550,9 @@ private: DiagnosticSink* _sink = nullptr; SerialSourceLocReader* _sourceLocReader = nullptr; SourceLoc _requestingSourceLoc; - RIFFSerialReader _riffReader; + ISerializerImpl* _reader = nullptr; - virtual ISerializerImpl* getBaseSerializer() override { return &_riffReader; } + virtual ISerializerImpl* getBaseSerializer() override { return _reader; } virtual void handleName(Name*& value) override; virtual void handleSourceLoc(SourceLoc& value) override; @@ -910,8 +911,21 @@ void writeSerializedModuleAST( // TODO: we might want to have a more careful pass here, // where we only encode the public declarations. - ASTEncodingContext context(cursor, moduleDecl, sourceLocWriter); - serialize(ASTSerializer(&context), moduleDecl); + BlobBuilder blobBuilder; + { + Fossil::SerialWriter writer(blobBuilder); + + ASTEncodingContext context(&writer, moduleDecl, sourceLocWriter); + serialize(ASTSerializer(&context), moduleDecl); + } + + ComPtr<ISlangBlob> blob; + blobBuilder.writeToBlob(blob.writeRef()); + + void const* data = blob->getBufferPointer(); + size_t size = blob->getBufferSize(); + + cursor.addDataChunk(PropertyKeys<Module>::ASTModule, data, size); } ModuleDecl* readSerializedModuleAST( @@ -922,8 +936,14 @@ ModuleDecl* readSerializedModuleAST( SerialSourceLocReader* sourceLocReader, SourceLoc requestingSourceLoc) { + auto dataChunk = as<RIFF::DataChunk>(chunk); + + auto rootVal = Fossil::getRootValue(dataChunk->getPayload(), dataChunk->getPayloadSize()); + + Fossil::SerialReader reader(rootVal); + ASTDecodingContext - context(linkage, astBuilder, sink, chunk, sourceLocReader, requestingSourceLoc); + context(linkage, astBuilder, sink, &reader, sourceLocReader, requestingSourceLoc); ModuleDecl* moduleDecl = nullptr; serialize(ASTSerializer(&context), moduleDecl); diff --git a/source/slang/slang-serialize-fossil.cpp b/source/slang/slang-serialize-fossil.cpp new file mode 100644 index 000000000..5af9d2e6e --- /dev/null +++ b/source/slang/slang-serialize-fossil.cpp @@ -0,0 +1,1636 @@ +// slang-serialize-fossil.cpp +#include "slang-serialize-fossil.h" + +#include "../core/slang-blob.h" + +namespace Slang +{ +namespace Fossil +{ + +// +// SerialWriter +// + +SerialWriter::SerialWriter(ChunkBuilder* chunk) + : _arena(4096) +{ + _initialize(chunk); +} + +SerialWriter::SerialWriter(BlobBuilder& blobBuilder) + : _arena(4096) +{ + auto chunk = blobBuilder.addChunk(); + _initialize(chunk); +} + +void SerialWriter::_initialize(ChunkBuilder* chunk) +{ + _blobBuilder = chunk->getParentBlob(); + + // The top-level structure consists of a header, + // and a root value. We will allocate a distinct + // chunk for each of them, with the header coming + // first. + // + auto headerChunk = chunk; + auto rootValueChunk = headerChunk->addChunkAfter(); + + // We will write the fields of the header chunk manually, + // although we will use a temporary of type `Fossil::Header` + // to help make sure we write them with the correct sizes. + // + Fossil::Header header; + memcpy(header.magic, Fossil::Header::kMagic, sizeof(Fossil::Header::kMagic)); + header.totalSizeIncludingHeader = 0; + header.flags = 0; + + headerChunk->writeData(&header.magic, sizeof(header.magic)); + headerChunk->writeData( + &header.totalSizeIncludingHeader, + sizeof(header.totalSizeIncludingHeader)); + headerChunk->writeData(&header.flags, sizeof(header.flags)); + + // The main reason we are writing the fields manually is + // that the last field of the header is a relative pointer + // to the root-value chunk. + // + headerChunk->writeRelativePtr<Fossil::RelativePtrOffset>(rootValueChunk); + + // The root value should always be a variant, and we want to + // set up to write into it in a reasonable way. + // + auto rootPtrLayout = _createLayout(FossilizedValKind::Ptr); + _state = State(rootPtrLayout, rootValueChunk); + + _pushVariantScope(); +} + + +SerialWriter::~SerialWriter() +{ + _popVariantScope(); + + _flush(); +} + +SerializationMode SerialWriter::getMode() +{ + return SerializationMode::Write; +} + +void SerialWriter::handleBool(bool& value) +{ + // A boolean value will be serialized as a full byte. + uint8_t v = value; + _writeSimpleValue(FossilizedValKind::Bool, v); +} + +void SerialWriter::handleInt8(int8_t& value) +{ + _writeSimpleValue(FossilizedValKind::Int8, value); +} + +void SerialWriter::handleInt16(int16_t& value) +{ + _writeSimpleValue(FossilizedValKind::Int16, value); +} + +void SerialWriter::handleInt32(Int32& value) +{ + _writeSimpleValue(FossilizedValKind::Int32, value); +} + +void SerialWriter::handleInt64(Int64& value) +{ + _writeSimpleValue(FossilizedValKind::Int64, value); +} + +void SerialWriter::handleUInt8(uint8_t& value) +{ + _writeSimpleValue(FossilizedValKind::UInt8, value); +} + +void SerialWriter::handleUInt16(uint16_t& value) +{ + _writeSimpleValue(FossilizedValKind::UInt16, value); +} + +void SerialWriter::handleUInt32(UInt32& value) +{ + _writeSimpleValue(FossilizedValKind::UInt32, value); +} + +void SerialWriter::handleUInt64(UInt64& value) +{ + _writeSimpleValue(FossilizedValKind::UInt64, value); +} + +void SerialWriter::handleFloat32(float& value) +{ + _writeSimpleValue(FossilizedValKind::Float32, value); +} + +void SerialWriter::handleFloat64(double& value) +{ + _writeSimpleValue(FossilizedValKind::Float64, value); +} + +void SerialWriter::handleString(String& value) +{ + auto size = value.getLength(); + if (_shouldEmitWithPointerIndirection(FossilizedValKind::String)) + { + if (size == 0) + { + _writeNull(); + return; + } + + if (auto found = _mapStringToChunk.tryGetValue(value)) + { + auto existingChunk = *found; + + auto ptrLayout = + (ContainerLayoutObj*)_reserveDestinationForWrite(FossilizedValKind::Ptr); + _mergeLayout(ptrLayout->baseLayout, FossilizedValKind::String); + + _commitWrite(ValInfo::relativePtrTo(existingChunk)); + return; + } + } + + _pushPotentiallyIndirectValueScope(FossilizedValKind::String); + + auto data = value.getBuffer(); + _writeValueRaw(ValInfo::rawData(data, size + 1, 1)); + + auto chunk = _popPotentiallyIndirectValueScope(); + + auto rawSize = UInt32(size); + chunk->addPrefixData(&rawSize, sizeof(rawSize)); + + _mapStringToChunk.addIfNotExists(value, chunk); +} + +void SerialWriter::beginArray() +{ + _pushContainerScope(FossilizedValKind::Array); +} + +void SerialWriter::endArray() +{ + _popContainerScope(); +} + +void SerialWriter::beginDictionary() +{ + _pushContainerScope(FossilizedValKind::Dictionary); +} + +void SerialWriter::endDictionary() +{ + _popContainerScope(); +} + +void SerialWriter::_pushContainerScope(FossilizedValKind kind) +{ + _pushPotentiallyIndirectValueScope(kind); +} + +void SerialWriter::_popContainerScope() +{ + auto elementCount = _state.elementCount; + auto containerChunk = _popPotentiallyIndirectValueScope(); + + if (containerChunk) + { + auto rawElementCount = UInt32(elementCount); + containerChunk->addPrefixData(&rawElementCount, sizeof(rawElementCount)); + } +} + +bool SerialWriter::hasElements() +{ + return false; +} + +void SerialWriter::beginStruct() +{ + _pushInlineValueScope(FossilizedValKind::Struct); +} + +void SerialWriter::endStruct() +{ + _popInlineValueScope(); +} + +void SerialWriter::beginVariant() +{ + _pushVariantScope(); + _pushInlineValueScope(FossilizedValKind::Struct); +} + +void SerialWriter::endVariant() +{ + _popInlineValueScope(); + _popVariantScope(); +} + +void SerialWriter::handleFieldKey(char const* name, Int index) +{ + // For now we are ignoring field keys, and treating + // structs as basically equivalent to tuples. + SLANG_UNUSED(name); + SLANG_UNUSED(index); +} + +void SerialWriter::beginTuple() +{ + _pushInlineValueScope(FossilizedValKind::Tuple); +} + +void SerialWriter::endTuple() +{ + _popInlineValueScope(); +} + +void SerialWriter::beginOptional() +{ + _pushIndirectValueScope(FossilizedValKind::Optional); +} + +void SerialWriter::endOptional() +{ + _popIndirectValueScope(); +} + +void SerialWriter::handleSharedPtr(void*& value, Callback callback, void* userData) +{ + // Because we are writing, we only care about the + // pointer that is already present in `value`. + // + void* liveObjectPtr = value; + + // The first special case we check for is a null pointer, + // which we can serialize as an inline value. + // + if (liveObjectPtr == nullptr) + { + _writeNull(); + return; + } + + // Next, we check to see if we have encountered this + // pointer before, in which case we've already allocated + // an index for it in the object definition list, and + // we can simply write a reference to that object. + // + if (auto found = _mapLiveObjectPtrToFossilizedObject.tryGetValue(liveObjectPtr)) + { + auto fossilizedObject = *found; + + _reserveDestinationForWrite(fossilizedObject->ptrLayout); + _commitWrite(ValInfo::relativePtrTo(fossilizedObject->chunk)); + + return; + } + + auto ptrLayout = _reserveDestinationForWrite(FossilizedValKind::Ptr); + auto chunk = _blobBuilder->addChunk(); + + auto fossilizedObject = new (_arena) FossilizedObjectInfo(); + fossilizedObject->chunk = chunk; + fossilizedObject->ptrLayout = ptrLayout; + fossilizedObject->liveObjectPtr = liveObjectPtr; + fossilizedObject->callback = callback; + fossilizedObject->userData = userData; + + _fossilizedObjects.add(fossilizedObject); + _mapLiveObjectPtrToFossilizedObject.add(liveObjectPtr, fossilizedObject); + + _commitWrite(ValInfo::relativePtrTo(chunk)); +} + +void SerialWriter::handleUniquePtr(void*& value, Callback callback, void* userData) +{ + // We treat all pointers as shared pointers, because there isn't really + // an optimized representation we would want to use for the unique case. + // + handleSharedPtr(value, callback, userData); +} + +void SerialWriter::handleDeferredObjectContents(void* valuePtr, Callback callback, void* userData) +{ + // Because we are already deferring writing of the *entirety* of + // an object's members as part of how `handleSharedPtr()` works, + // we don't need to implement deferral at this juncture. + // + // (In practice the `handleDeferredObjectContents()` operation is + // more for the benefit of reading than writing). + // + callback(valuePtr, userData); +} + +SerialWriter::LayoutObj* SerialWriter::_createSimpleLayout(FossilizedValKind kind) +{ + switch (kind) + { + case FossilizedValKind::Bool: + case FossilizedValKind::Int8: + case FossilizedValKind::UInt8: + return new (_arena) SimpleLayoutObj(kind, 1); + + case FossilizedValKind::Int16: + case FossilizedValKind::UInt16: + return new (_arena) SimpleLayoutObj(kind, 2); + + case FossilizedValKind::Int32: + case FossilizedValKind::UInt32: + case FossilizedValKind::Float32: + return new (_arena) SimpleLayoutObj(kind, 4); + + case FossilizedValKind::Int64: + case FossilizedValKind::UInt64: + case FossilizedValKind::Float64: + return new (_arena) SimpleLayoutObj(kind, 8); + + case FossilizedValKind::String: + return new (_arena) SimpleLayoutObj(kind); + + default: + SLANG_UNEXPECTED("unhandled case"); + UNREACHABLE_RETURN(nullptr); + } +} + +SerialWriter::LayoutObj* SerialWriter::_createLayout(FossilizedValKind kind) +{ + switch (kind) + { + case FossilizedValKind::Array: + case FossilizedValKind::Optional: + case FossilizedValKind::Dictionary: + return new (_arena) ContainerLayoutObj(kind, nullptr); + + case FossilizedValKind::Ptr: + return new (_arena) ContainerLayoutObj( + kind, + nullptr, + sizeof(Fossil::RelativePtrOffset), + sizeof(Fossil::RelativePtrOffset)); + + case FossilizedValKind::Struct: + case FossilizedValKind::Tuple: + return new (_arena) RecordLayoutObj(kind); + + case FossilizedValKind::Variant: + // A variant is being treated like a container in this context, + // because it wants to be able to track the layout of what it + // ended up holding... + // + return new (_arena) ContainerLayoutObj(kind, nullptr); + + case FossilizedValKind::Bool: + case FossilizedValKind::Int8: + case FossilizedValKind::Int16: + case FossilizedValKind::Int32: + case FossilizedValKind::Int64: + case FossilizedValKind::UInt8: + case FossilizedValKind::UInt16: + case FossilizedValKind::UInt32: + case FossilizedValKind::UInt64: + case FossilizedValKind::Float32: + case FossilizedValKind::Float64: + case FossilizedValKind::String: + { + if (auto found = _simpleLayouts.tryGetValue(kind)) + return *found; + + auto layout = _createSimpleLayout(kind); + _simpleLayouts.add(kind, layout); + return layout; + } + + default: + SLANG_UNEXPECTED("unhandled case"); + UNREACHABLE_RETURN(nullptr); + } +} + +SerialWriter::LayoutObj* SerialWriter::_mergeLayout(LayoutObj*& dst, FossilizedValKind kind) +{ + if (!dst) + { + dst = _createLayout(kind); + } + + if (dst->kind != kind) + { + SLANG_UNEXPECTED("type mismatch during serialization"); + } + + // As a special case, if the right-hand-side is a variant, + // then we want to have a unique layout object for each + // instance. + // + if (kind == FossilizedValKind::Variant) + { + auto src = _createLayout(kind); + return src; + } + + return dst; +} + +void SerialWriter::_mergeLayout(LayoutObj*& dst, LayoutObj* src) +{ + if (dst == src) + return; + + if (!src) + return; + + if (!dst) + { + dst = src; + return; + } + + _mergeLayout(dst, src->getKind()); + + switch (src->getKind()) + { + case FossilizedValKind::Array: + case FossilizedValKind::Optional: + case FossilizedValKind::Dictionary: + case FossilizedValKind::Ptr: + { + auto dstContainer = (ContainerLayoutObj*)dst; + auto srcContainer = (ContainerLayoutObj*)src; + _mergeLayout(dstContainer->baseLayout, srcContainer->baseLayout); + } + break; + + case FossilizedValKind::String: + break; + + case FossilizedValKind::Variant: + // Recursive merging should not be applied to variants; + // each variant is unique until later deduplication. + break; + + default: + SLANG_UNEXPECTED("unhandled case"); + break; + } +} + +SerialWriter::RecordLayoutObj::FieldInfo& SerialWriter::_getOrAddField( + RecordLayoutObj* recordLayout, + Index index) +{ + // Note: we are doing all the allocation for `LayoutObj`s from + // an arena, so that we don't have to worry about managing + // their lifetimes carefully. + // + // One place where that is a bit tedious is handling the storage + // for the array of fields for a record. + // + // TODO(tfoley): see if there's allocator support on `List<T>` + // or similar, so that it can be made to just use the arena. + + SLANG_ASSERT(recordLayout); + SLANG_ASSERT(index >= 0); + + if (index < recordLayout->fieldCount) + return recordLayout->fields[index]; + + SLANG_ASSERT(index == recordLayout->fieldCount); + + if (index >= recordLayout->fieldCapacity) + { + if (recordLayout->fieldCapacity == 0) + recordLayout->fieldCapacity = 16; + + while (index >= recordLayout->fieldCapacity) + { + recordLayout->fieldCapacity = (recordLayout->fieldCapacity * 3) >> 1; + } + + auto newFields = new (_arena) RecordLayoutObj::FieldInfo[recordLayout->fieldCapacity]; + for (Index i = 0; i < recordLayout->fieldCount; ++i) + newFields[i] = recordLayout->fields[i]; + recordLayout->fields = newFields; + } + + recordLayout->fields[recordLayout->fieldCount++] = RecordLayoutObj::FieldInfo(); + return recordLayout->fields[index]; +} + +SerialWriter::ValInfo SerialWriter::ValInfo::rawData(void const* data, Size size, Size alignment) +{ + ValInfo val(Kind::RawData); + val.data.ptr = data; + val.data.size = size; + val.data.alignment = alignment; + return val; +} + +SerialWriter::ValInfo SerialWriter::ValInfo::relativePtrTo(ChunkBuilder* targetChunk) +{ + ValInfo val(Kind::RelativePtr); + val.chunk = targetChunk; + return val; +} + +SerialWriter::ValInfo SerialWriter::ValInfo::contentsOf(ChunkBuilder* chunk) +{ + ValInfo val(Kind::ContentsOfChunk); + val.chunk = chunk; + return val; +} + +Size SerialWriter::ValInfo::getAlignment() const +{ + switch (kind) + { + case Kind::RelativePtr: + return sizeof(Fossil::RelativePtrOffset); + + case Kind::ContentsOfChunk: + return chunk->getAlignment(); + + case Kind::RawData: + return data.alignment; + + default: + SLANG_UNEXPECTED("unhandled case"); + break; + } +} + +void SerialWriter::_pushInlineValueScope(FossilizedValKind kind) +{ + auto layout = _reserveDestinationForWrite(kind); + _pushState(layout); +} + +void SerialWriter::_popInlineValueScope() +{ + auto layout = _state.layout; + auto chunk = _state.chunk; + + if (chunk) + { + if (layout->size == 0) + { + layout->size = chunk->getContentSize(); + } + SLANG_ASSERT(layout->size == chunk->getContentSize()); + } + + _popState(); + + _commitWrite(ValInfo::contentsOf(chunk)); +} + +void SerialWriter::_pushVariantScope() +{ + _pushPotentiallyIndirectValueScope(FossilizedValKind::Variant); +} + +void SerialWriter::_popVariantScope() +{ + SLANG_ASSERT(_state.layout); + SLANG_ASSERT(_state.layout->kind == FossilizedValKind::Variant); + auto variantLayout = (ContainerLayoutObj*)_state.layout; + auto valueLayout = variantLayout->baseLayout; + SLANG_ASSERT(valueLayout); + + auto variantChunk = _popPotentiallyIndirectValueScope(); + + // The key feature of a variant is that it carries its own + // layout information. + // + // We need to insert a pointer to the serialized form + // of the layout information for the element type as a header + // *before* the content. + // + // The first step there is to turn the element layout into + // a handle such that we can write a relative pointer to it. + // + + VariantInfo variantInfo; + variantInfo.layout = valueLayout; + variantInfo.chunk = variantChunk; + _variants.add(variantInfo); +} + + +void SerialWriter::_pushPotentiallyIndirectValueScope(FossilizedValKind kind) +{ + if (_shouldEmitWithPointerIndirection(kind)) + { + _pushIndirectValueScope(kind); + } + else + { + _pushInlineValueScope(kind); + } +} + +ChunkBuilder* SerialWriter::_popPotentiallyIndirectValueScope() +{ + // TODO(tfoley): Try to make this function just be a simple + // conditional to select between the functions for the + // indirect and inline cases. + + auto valueLayout = _state.layout; + auto valueChunk = _state.chunk; + _popState(); + + auto valueKind = valueLayout->getKind(); + if (_shouldEmitWithPointerIndirection(valueKind)) + { + return _writeKnownIndirectValueSharedLogic(valueChunk); + } + else + { + _commitWrite(ValInfo::contentsOf(valueChunk)); + return _state.chunk; + } +} + +void SerialWriter::_pushIndirectValueScope(FossilizedValKind kind) +{ + auto ptrLayout = (ContainerLayoutObj*)_reserveDestinationForWrite(FossilizedValKind::Ptr); + auto valueLayout = _mergeLayout(ptrLayout->baseLayout, kind); + + _pushState(valueLayout); +} + +void SerialWriter::_popIndirectValueScope() +{ + auto valueChunk = _state.chunk; + _popState(); + + _writeKnownIndirectValueSharedLogic(valueChunk); +} + +ChunkBuilder* SerialWriter::_writeKnownIndirectValueSharedLogic(ChunkBuilder* valueChunk) +{ + if (!valueChunk) + { + _commitWrite(ValInfo::relativePtrTo(nullptr)); + return nullptr; + } + + _blobBuilder->addChunk(valueChunk); + + _commitWrite(ValInfo::relativePtrTo(valueChunk)); + return valueChunk; +} + + +void SerialWriter::_pushState(LayoutObj* layout) +{ + _stack.add(_state); + _state = State(layout); +} + +void SerialWriter::_popState() +{ + SLANG_ASSERT(_stack.getCount() != 0); + _state = _stack.getLast(); + _stack.removeLast(); +} + +void SerialWriter::_ensureChunkExists() +{ + if (_state.chunk != nullptr) + return; + + _state.chunk = _blobBuilder->createUnparentedChunk(); +} + +void SerialWriter::_writeValueRaw(ValInfo const& val) +{ + switch (val.kind) + { + case ValInfo::Kind::RawData: + if (val.data.size == 0) + return; + _ensureChunkExists(); + _state.chunk->writePaddingToAlignTo(val.data.alignment); + _state.chunk->writeData(val.data.ptr, val.data.size); + break; + + case ValInfo::Kind::RelativePtr: + _ensureChunkExists(); + _state.chunk->writeRelativePtr<Fossil::RelativePtrOffset>(val.chunk); + break; + + case ValInfo::Kind::ContentsOfChunk: + { + if (!_state.chunk) + { + _state.chunk = val.chunk; + } + else + { + _state.chunk->addContentsOf(val.chunk); + } + } + break; + + default: + SLANG_UNEXPECTED("unknown Fossil::SerialWriter::ValInfo::Kind"); + break; + } +} + +bool SerialWriter::_shouldEmitWithPointerIndirection(FossilizedValKind kind) +{ + switch (kind) + { + default: + return false; + + case FossilizedValKind::Optional: + return true; + + case FossilizedValKind::Array: + case FossilizedValKind::Dictionary: + case FossilizedValKind::String: + case FossilizedValKind::Variant: + break; + } + + switch (_state.layout->getKind()) + { + default: + return true; + + case FossilizedValKind::Optional: + case FossilizedValKind::Ptr: + return false; + } +} + +SerialWriter::LayoutObj*& SerialWriter::_reserveDestinationForWrite() +{ + switch (_state.layout->getKind()) + { + case FossilizedValKind::Struct: + case FossilizedValKind::Tuple: + { + auto recordLayout = (RecordLayoutObj*)_state.layout; + auto elementIndex = _state.elementCount; + auto& elementLayout = _getOrAddField(recordLayout, elementIndex).layout; + return elementLayout; + } + break; + + case FossilizedValKind::Ptr: + case FossilizedValKind::Optional: + case FossilizedValKind::Array: + case FossilizedValKind::Dictionary: + case FossilizedValKind::Variant: + { + auto containerLayout = (ContainerLayoutObj*)_state.layout; + auto& elementLayout = containerLayout->baseLayout; + return elementLayout; + } + break; + + default: + SLANG_UNEXPECTED("unhandled case"); + break; + } +} + +SerialWriter::LayoutObj* SerialWriter::_reserveDestinationForWrite(FossilizedValKind srcKind) +{ + return _mergeLayout(_reserveDestinationForWrite(), srcKind); +} + +SerialWriter::LayoutObj* SerialWriter::_reserveDestinationForWrite(LayoutObj* srcLayout) +{ + SLANG_ASSERT(srcLayout != nullptr); + _mergeLayout(_reserveDestinationForWrite(), srcLayout); + return srcLayout; +} + +void SerialWriter::_commitWrite(ValInfo const& val) +{ + auto outerKind = _state.layout->getKind(); + switch (outerKind) + { + case FossilizedValKind::Struct: + case FossilizedValKind::Tuple: + { + auto recordLayout = (RecordLayoutObj*)_state.layout; + auto elementIndex = _state.elementCount++; + auto& fieldInfo = _getOrAddField(recordLayout, elementIndex); + + Size fieldOffset = 0; + if (elementIndex != 0) + { + auto chunk = _state.chunk; + chunk->writePaddingToAlignTo(val.getAlignment()); + + fieldOffset = chunk->getContentSize(); + } + fieldInfo.offset = fieldOffset; + + _writeValueRaw(val); + } + break; + + case FossilizedValKind::Optional: + case FossilizedValKind::Ptr: + case FossilizedValKind::Array: + case FossilizedValKind::Dictionary: + case FossilizedValKind::Variant: + { + auto elementIndex = _state.elementCount++; + + switch (outerKind) + { + case FossilizedValKind::Optional: + case FossilizedValKind::Ptr: + if (elementIndex > 0) + { + SLANG_UNEXPECTED( + "error during serialization: optional with more than one value inside!!"); + } + break; + + default: + break; + } + + _writeValueRaw(val); + } + break; + + default: + SLANG_UNEXPECTED("unhandled case"); + break; + } +} + +void SerialWriter::_writeSimpleValue( + FossilizedValKind kind, + void const* data, + size_t size, + size_t alignment) +{ + auto layout = _reserveDestinationForWrite(kind); + SLANG_ASSERT(layout->size == size); + SLANG_ASSERT(layout->alignment = alignment); + _commitWrite(ValInfo::rawData(data, size, alignment)); +} + +void SerialWriter::_writeNull() +{ + RelativePtrOffset offset = 0; + _writeSimpleValue(FossilizedValKind::Ptr, offset); +} + +void SerialWriter::_flush() +{ + while (_writtenObjectDefinitionCount < _fossilizedObjects.getCount()) + { + auto objectIndex = _writtenObjectDefinitionCount++; + auto fossilizedObject = _fossilizedObjects[objectIndex]; + + SLANG_ASSERT(fossilizedObject->liveObjectPtr); + + _state = State(fossilizedObject->ptrLayout, fossilizedObject->chunk); + + fossilizedObject->callback(&fossilizedObject->liveObjectPtr, fossilizedObject->userData); + } + + // Once we've written out all the payload data, we can start to work on + // serializing layout information for all the variant values that were + // written. + // + for (auto variantInfo : _variants) + { + auto layoutChunk = _getOrCreateChunkForLayout(variantInfo.layout); + variantInfo.chunk->addPrefixRelativePtr<Fossil::RelativePtrOffset>(layoutChunk); + } +} + +ChunkBuilder* SerialWriter::_getOrCreateChunkForLayout(LayoutObj* layout) +{ + if (!layout) + return nullptr; + + // We start by looking for an existing chunk for `layout`, + // which would be cached on the object itself. + // + if (auto existingChunk = layout->chunk) + return existingChunk; + + // Next we look for an existing chunk that matches the + // structure of `layout`. + // + LayoutObjKey key = {layout}; + if (auto found = _mapLayoutObjToChunk.tryGetValue(key)) + { + auto existingChunk = *found; + layout->chunk = existingChunk; + return existingChunk; + } + + // If no existing layout has been written to a chunk, + // then we'll create one. + // + auto chunk = _blobBuilder->addChunk(); + layout->chunk = chunk; + _mapLayoutObjToChunk.add(key, chunk); + + auto kind = layout->getKind(); + auto rawKind = UInt32(kind); + chunk->writeData(&rawKind, sizeof(rawKind)); + + switch (kind) + { + default: + break; + + case FossilizedValKind::Ptr: + case FossilizedValKind::Optional: + { + auto containerLayout = (ContainerLayoutObj*)layout; + auto elementLayout = containerLayout->baseLayout; + auto elementLayoutChunk = _getOrCreateChunkForLayout(elementLayout); + chunk->writeRelativePtr<Fossil::RelativePtrOffset>(elementLayoutChunk); + } + break; + + case FossilizedValKind::Array: + case FossilizedValKind::Dictionary: + { + auto containerLayout = (ContainerLayoutObj*)layout; + auto elementLayout = containerLayout->baseLayout; + auto elementLayoutChunk = _getOrCreateChunkForLayout(elementLayout); + chunk->writeRelativePtr<Fossil::RelativePtrOffset>(elementLayoutChunk); + + UInt32 elementStride = 0; + if (elementLayout) + { + elementStride = + UInt32(roundUpToAlignment(elementLayout->size, elementLayout->alignment)); + SLANG_ASSERT(elementStride != 0); + } + chunk->writeData(&elementStride, sizeof(elementStride)); + } + break; + + case FossilizedValKind::Struct: + case FossilizedValKind::Tuple: + { + auto recordLayout = (RecordLayoutObj*)layout; + + auto fieldCount = UInt32(recordLayout->fieldCount); + chunk->writeData(&fieldCount, sizeof(fieldCount)); + + for (Index i = 0; i < fieldCount; ++i) + { + auto& field = recordLayout->fields[i]; + auto fieldLayoutChunk = _getOrCreateChunkForLayout(field.layout); + chunk->writeRelativePtr<Fossil::RelativePtrOffset>(fieldLayoutChunk); + + auto fieldOffset = UInt32(field.offset); + chunk->writeData(&fieldOffset, sizeof(fieldOffset)); + + if (i != 0) + { + // Make sure that all but the first field have + // a non-zero offset, to validate that offsets + // are being comptued at all. + // + SLANG_ASSERT(fieldOffset != 0); + } + } + } + break; + } + + return chunk; +} + +bool SerialWriter::LayoutObjKey::operator==(LayoutObjKey const& that) const +{ + if (obj == that.obj) + return true; + + if (!obj || !that.obj) + return false; + + SLANG_ASSERT(obj && that.obj); + + if (obj->kind != that.obj->kind) + return false; + + switch (obj->kind) + { + default: + break; + + case FossilizedValKind::Array: + case FossilizedValKind::Dictionary: + case FossilizedValKind::Optional: + case FossilizedValKind::Ptr: + { + auto thisContainer = (ContainerLayoutObj*)obj; + auto thatContainer = (ContainerLayoutObj*)that.obj; + + LayoutObjKey thisElement = thisContainer->baseLayout; + LayoutObjKey thatElement = thatContainer->baseLayout; + + if (thisElement != thatElement) + return false; + } + break; + + case FossilizedValKind::Tuple: + case FossilizedValKind::Struct: + { + auto thisRecord = (RecordLayoutObj*)obj; + auto thatRecord = (RecordLayoutObj*)that.obj; + + if (thisRecord->fieldCount != thatRecord->fieldCount) + return false; + + auto fieldCount = thisRecord->fieldCount; + for (Index i = 0; i < fieldCount; ++i) + { + auto thisField = thisRecord->fields[i]; + auto thatField = thatRecord->fields[i]; + + if (thisField.offset != thatField.offset) + return false; + + LayoutObjKey thisFieldLayout = thisField.layout; + LayoutObjKey thatFieldLayout = thatField.layout; + + if (thisFieldLayout != thatFieldLayout) + return false; + } + } + break; + } + + return true; +} + +bool SerialWriter::LayoutObjKey::operator!=(LayoutObjKey const& that) const +{ + return !(*this == that); +} + +HashCode64 SerialWriter::LayoutObjKey::getHashCode() const +{ + Hasher hasher; + hashInto(hasher); + return hasher.getResult(); +} + +void SerialWriter::LayoutObjKey::hashInto(Hasher& hasher) const +{ + if (!obj) + { + hasher.hashValue(obj); + return; + } + + hasher.hashValue(obj->kind); + + switch (obj->kind) + { + default: + break; + + case FossilizedValKind::Array: + case FossilizedValKind::Dictionary: + case FossilizedValKind::Optional: + case FossilizedValKind::Ptr: + { + auto container = (ContainerLayoutObj*)obj; + + LayoutObjKey(container->baseLayout).hashInto(hasher); + } + break; + + case FossilizedValKind::Tuple: + case FossilizedValKind::Struct: + { + auto record = (RecordLayoutObj*)obj; + + auto fieldCount = record->fieldCount; + hasher.hashValue(record->fieldCount); + + for (Index i = 0; i < fieldCount; ++i) + { + auto& field = record->fields[i]; + hasher.hashValue(field.offset); + LayoutObjKey(field.layout).hashInto(hasher); + } + } + break; + } +} + + +// +// SerialReader +// + +SerialReader::SerialReader(FossilizedValRef valRef) +{ + _state.type = State::Type::Root; + _state.baseValue = valRef; + _state.elementIndex = 0; + _state.elementCount = 1; +} + +SerialReader::~SerialReader() +{ + _flush(); +} + +SerializationMode SerialReader::getMode() +{ + return SerializationMode::Read; +} + +void SerialReader::handleBool(bool& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedBoolVal>(valRef)->getValue(); +} + +void SerialReader::handleInt8(int8_t& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedInt8Val>(valRef)->getValue(); +} + +void SerialReader::handleInt16(int16_t& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedInt16Val>(valRef)->getValue(); +} + +void SerialReader::handleInt32(Int32& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedInt32Val>(valRef)->getValue(); +} + +void SerialReader::handleInt64(Int64& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedInt64Val>(valRef)->getValue(); +} + +void SerialReader::handleUInt8(uint8_t& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedUInt8Val>(valRef)->getValue(); +} + +void SerialReader::handleUInt16(uint16_t& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedUInt16Val>(valRef)->getValue(); +} + +void SerialReader::handleUInt32(UInt32& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedUInt32Val>(valRef)->getValue(); +} + +void SerialReader::handleUInt64(UInt64& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedUInt64Val>(valRef)->getValue(); +} + +void SerialReader::handleFloat32(float& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedFloat32Val>(valRef)->getValue(); +} + +void SerialReader::handleFloat64(double& value) +{ + auto valRef = _readValRef(); + value = as<FossilizedFloat64Val>(valRef)->getValue(); +} + +void SerialReader::handleString(String& value) +{ + auto valRef = _readPotentiallyIndirectValRef(); + if (!valRef) + { + value = String(); + } + else + { + value = as<FossilizedStringObj>(valRef)->getValue(); + } +} + +void SerialReader::beginArray() +{ + auto valRef = _readPotentiallyIndirectValRef(); + auto arrayRef = as<FossilizedContainerObj>(valRef); + + _pushState(); + + _state.type = State::Type::Array; + _state.baseValue = valRef; + _state.elementIndex = 0; + _state.elementCount = getElementCount(arrayRef); +} + +void SerialReader::endArray() +{ + _popState(); +} + +void SerialReader::beginDictionary() +{ + auto valRef = _readPotentiallyIndirectValRef(); + auto dictionaryRef = as<FossilizedContainerObj>(valRef); + + _pushState(); + + _state.type = State::Type::Dictionary; + _state.baseValue = valRef; + _state.elementIndex = 0; + _state.elementCount = getElementCount(dictionaryRef); +} + +void SerialReader::endDictionary() +{ + _popState(); +} + +bool SerialReader::hasElements() +{ + return _state.elementIndex < _state.elementCount; +} + +void SerialReader::beginStruct() +{ + auto valRef = _readValRef(); + auto recordRef = as<FossilizedRecordVal>(valRef); + + _pushState(); + + _state.type = State::Type::Struct; + _state.baseValue = valRef; + _state.elementIndex = 0; + _state.elementCount = getFieldCount(recordRef); +} + +void SerialReader::endStruct() +{ + _popState(); +} + +void SerialReader::beginVariant() +{ + auto valRef = _readPotentiallyIndirectValRef(); + auto variantRef = as<FossilizedVariantObj>(valRef); + + auto contentValRef = getVariantContent(variantRef); + auto contentRecordRef = as<FossilizedRecordVal>(contentValRef); + + _pushState(); + + _state.type = State::Type::Struct; + _state.baseValue = contentValRef; + _state.elementIndex = 0; + _state.elementCount = getFieldCount(contentRecordRef); +} + +void SerialReader::endVariant() +{ + _popState(); +} + +void SerialReader::handleFieldKey(char const* name, Int index) +{ + // For now we are ignoring field keys, and treating + // structs as basically equivalent to tuples. + SLANG_UNUSED(name); + SLANG_UNUSED(index); +} + +void SerialReader::beginTuple() +{ + auto valRef = _readValRef(); + auto recordRef = as<FossilizedRecordVal>(valRef); + + _pushState(); + + _state.type = State::Type::Tuple; + _state.baseValue = valRef; + _state.elementIndex = 0; + _state.elementCount = getFieldCount(recordRef); +} + +void SerialReader::endTuple() +{ + _popState(); +} + +void SerialReader::beginOptional() +{ + auto valRef = _readIndirectValRef(); + auto optionalRef = as<FossilizedOptionalObj>(valRef); + + _pushState(); + + _state.type = State::Type::Optional; + _state.baseValue = valRef; + _state.elementIndex = 0; + _state.elementCount = Count(hasValue(optionalRef)); +} + +void SerialReader::endOptional() +{ + _popState(); +} + +void SerialReader::handleSharedPtr(void*& value, Callback callback, void* userData) +{ + // The fossilized value at our cursor must be a pointer, + // and we can resolve what it is pointing to easily enough. + // + auto valRef = _readValRef(); + auto ptrRef = as<FossilizedPtrVal>(valRef); + auto targetValRef = getPtrTarget(ptrRef); + + // The logic here largely mirrors what appears in + // `SerialWriter::handleSharedPtr`. + // + // We first check for an explicitly written null pointer. + // If we find one our work is very easy. + // + if (!targetValRef) + { + value = nullptr; + return; + } + + // Now we need to check if we've previously read in + // a reference to the same object. + // + if (auto found = _mapFossilizedObjectPtrToObjectInfo.tryGetValue(targetValRef.getData())) + { + auto objectInfo = *found; + + // We've seen this object before, although it + // is still possible that we are in the middle + // of reading it as part of an invocation + // of `handleSharedPtr()` further up the call + // stack. + // + // If a non-nullpointer value has already been + // written into the `objectInfo`, then that means + // the callback that was run for the prior (or + // in-flight) read operation has already allocated + // or found an object and written it out. + // In that case we will trust the value. + // + if (objectInfo->resurrectedObjectPtr == nullptr) + { + // It is possible that the pointer is null because + // the callback that was invoked explicitly *chose* + // to yield a null pointer (perhaps the application + // is choosing not to deserialize some optional + // piece of state). + // + // However, if there is still a callback in-flight + // to read this object, and the pointer is null, + // then we have reached a circular reference, + // and need to signal an error. + // + if (objectInfo->state == ObjectState::ReadingInProgress) + { + SLANG_UNEXPECTED("circularity detected in fossil deserialization"); + } + } + value = objectInfo->resurrectedObjectPtr; + return; + } + + // At this point we are reading a reference to an + // object index that has not yet been read at all. + // + auto objectInfo = RefPtr(new ObjectInfo()); + _mapFossilizedObjectPtrToObjectInfo.add(targetValRef.getData(), objectInfo); + + objectInfo->fossilizedObjectRef = targetValRef; + + // We cannot return from this function until we have + // stored a pointer into `value`, to represent the + // deserialized object. + // + // Thus we will set ourselves up to start reading + // from the relevant object definition, and invoke + // the callback that was passed in. + // + // Calling into user-defined serialization logic from + // within this function creates the possibility of + // unbounded/infinite recursion, so it is vital that + // the user is properly using `deferSerializeObjectContents()` + // to delay reading data that isn't immediately + // necessary. + // + // We will still set the `objectInfo.state` to reflect + // this in-flight operation so that we can detect + // a cirularity if one occurs at runtime. + // + objectInfo->state = ObjectState::ReadingInProgress; + + // We save/restore the current cursor around + // the callback, because we need to be able + // to return to the current state to continue + // reading whatever comes after the pointer + // we were invoked to read. + // + _pushState(); + _state.type = State::Type::Object; + _state.baseValue = objectInfo->fossilizedObjectRef; + _state.elementIndex = 0; + _state.elementCount = 1; + + // Note that we are passing the address of `objectInfo.ptr`, + // and `objectInfo` is a reference to an element of the + // `_objects` array. Thus whenever the `callback` stores + // a pointer into that output parameter, the value it writes + // will automatically be visible to any subsequent calls + // to `handleSharedPtr()`, even if they occur before + // `callback` returns. + // + // Thus a "true" circularity can only occur if the callback + // recursively reads a reference to the same object again + // *before* it allocates the in-memory representation of + // that objects and stores a pointer to it into the output + // parameter. + // + callback(&objectInfo->resurrectedObjectPtr, userData); + + _popState(); + + objectInfo->state = ObjectState::ReadingComplete; + + value = objectInfo->resurrectedObjectPtr; +} + +void SerialReader::handleUniquePtr(void*& value, Callback callback, void* userData) +{ + // We treat all pointers as shared pointers, because there isn't really + // an optimized representation we would want to use for the unique case. + // + handleSharedPtr(value, callback, userData); +} + +void SerialReader::handleDeferredObjectContents(void* valuePtr, Callback callback, void* userData) +{ + // Unlike the case in `SerialWriter::handleDeferredObjectContents()`, + // we very much *do* want to delay invoking the callback until later. + // + // There is a kind of symmetry going on, where the writer delays the + // callback passed to `handleSharedPtr()`, but *not* the callback + // passed to `handleDeferredObjectContents()`, while the reader + // does the opposite: immediately calls the callback in `handleSharedPtr()` + // but delays calling it here. + + // We make sure to save the current `_cursor` value along with + // the arguments that will be passed into the callback, so that + // we can restore the reader to this state before invoking + // the callbak in `_flush()`. + + DeferredAction deferredAction; + deferredAction.savedState = _state; + deferredAction.resurrectedObjectPtr = valuePtr; + deferredAction.callback = callback; + deferredAction.userData = userData; + + _deferredActions.add(deferredAction); +} + +void SerialReader::_flush() +{ + // We need to flush any actions that were deferred + // and are still pending. + // + while (_deferredActions.getCount() != 0) + { + // TODO: For simplicity we are using the `_deferredActions` + // array as a stack (LIFO), but it would be good to + // check whether there is a menaingful difference in how + // large the array would need to grow for a FIFO vs. LIFO, + // and pick the better option. + // + auto deferredAction = _deferredActions.getLast(); + _deferredActions.removeLast(); + + _state = deferredAction.savedState; + deferredAction.callback(deferredAction.resurrectedObjectPtr, deferredAction.userData); + } +} + +FossilizedValRef SerialReader::_readValRef() +{ + switch (_state.type) + { + case State::Type::Root: + case State::Type::Object: + SLANG_ASSERT(_state.elementCount == 1); + SLANG_ASSERT(_state.elementIndex == 0); + _state.elementIndex++; + return _state.baseValue; + + case State::Type::Struct: + case State::Type::Tuple: + { + SLANG_ASSERT(_state.elementIndex < _state.elementCount); + auto index = _state.elementIndex++; + + auto recordRef = as<FossilizedRecordVal>(_state.baseValue); + return getField(recordRef, index); + } + + case State::Type::Optional: + { + SLANG_ASSERT(_state.elementCount == 1); + SLANG_ASSERT(_state.elementIndex == 0); + + auto optionalRef = as<FossilizedOptionalObj>(_state.baseValue); + return getValue(optionalRef); + } + + case State::Type::Array: + case State::Type::Dictionary: + { + SLANG_ASSERT(_state.elementIndex < _state.elementCount); + auto index = _state.elementIndex++; + + auto containerRef = as<FossilizedContainerObj>(_state.baseValue); + return getElement(containerRef, index); + } + + default: + SLANG_UNEXPECTED("unhandled case"); + break; + } +} + +FossilizedValRef SerialReader::_readIndirectValRef() +{ + auto ptrValRef = _readValRef(); + auto ptrRef = as<FossilizedPtrVal>(ptrValRef); + + auto valRef = getPtrTarget(ptrRef); + return valRef; +} + + +FossilizedValRef SerialReader::_readPotentiallyIndirectValRef() +{ + auto valRef = _readValRef(); + if (auto ptrRef = as<FossilizedPtrVal>(valRef)) + { + return getPtrTarget(ptrRef); + } + return valRef; +} + +void SerialReader::_pushState() +{ + _stack.add(_state); +} + +void SerialReader::_popState() +{ + SLANG_ASSERT(_stack.getCount() != 0); + _state = _stack.getLast(); + _stack.removeLast(); +} + +} // namespace Fossil +} // namespace Slang diff --git a/source/slang/slang-serialize-fossil.h b/source/slang/slang-serialize-fossil.h new file mode 100644 index 000000000..0393c5784 --- /dev/null +++ b/source/slang/slang-serialize-fossil.h @@ -0,0 +1,741 @@ +// slang-serialize-fossil.h +#ifndef SLANG_SERIALIZE_FOSSIL_H +#define SLANG_SERIALIZE_FOSSIL_H + +// +// This file provides implementations of `ISerializerImpl` that +// serialize hierarchical data in the "memory-mappable" binary +// format defined in `slang-fossil.h`. +// + +#include "../core/slang-blob-builder.h" +#include "../core/slang-internally-linked-list.h" +#include "../core/slang-io.h" +#include "../core/slang-memory-arena.h" +#include "../core/slang-relative-ptr.h" +#include "slang-fossil.h" +#include "slang-serialize.h" + +namespace Slang +{ +namespace Fossil +{ + +/// Serializer implementation for writing objects to a fossil-format blob. +struct SerialWriter : ISerializerImpl +{ +public: + SerialWriter(ChunkBuilder* chunk); + SerialWriter(BlobBuilder& blobBuilder); + + ~SerialWriter(); + +private: + SerialWriter() = delete; + + void _initialize(ChunkBuilder* chunk); + + // The fossil format stores layout information, but that + // information is kept separate from the values themselves. + // + // The nature of the `ISerializer` interface means that we + // can only discover the layout as it is first being written, + // so we need an intermediate representation of layouts + // that we compute during the serialization process, before + // we can write those layouts out as their own bytes. + // + // Two related issues make this task a little intricate: + // + // * We don't want to redundantly serialize many copies of + // the same layout (since the whole point of keeping the + // layout information separate from the content is to + // save on space), and ideally we don't want to *create* + // a large number of intermediate layouts that will end + // up getting deduplicated out of existence. + // + // * If the same C++ type is getting serialized multiple times + // (e.g., in a loop serializing an array) we both want to + // re-use the layout from the first element for subsequent + // elements *and* we want to handle the case where parts of + // the layout get expanded on subsequent iterations (e.g., + // the first element in an array might have contained a null + // pointer, so there is no layout info for what it points to, + // but a later element might fill in that gap). + // + // The `_mergeLayouts()` operation is central to how these + // issues are handled, allowing code to attach new information + // to an existing layout as it goes. + + /// Representation of a layout for data that has been serialized. + class LayoutObj + { + public: + LayoutObj(FossilizedValKind kind, Size size = 0, Size alignment = 1) + : kind(kind), size(size), alignment(alignment) + { + } + + virtual ~LayoutObj() {} + + FossilizedValKind getKind() const { return kind; } + + Size getSize() const { return size; } + Size getAlignment() const { return alignment; } + + FossilizedValKind kind; + Size size = 0; + Size alignment = 1; + + /// If this layout is getting serialized out, then this + /// is a pointer to the chunk that will store the `FossilizedValLayout`. + /// + ChunkBuilder* chunk = nullptr; + }; + + /// Create a layout of the given `kind`. + /// + /// If `kind` is one of the simple layout kinds, then this will + /// return a singleton layout. + /// + LayoutObj* _createLayout(FossilizedValKind kind); + + LayoutObj* _createSimpleLayout(FossilizedValKind kind); + Dictionary<FossilizedValKind, LayoutObj*> _simpleLayouts; + + // Rather than try to do detailed memory management for + // layouts, we simply allocate them from an arena. + + MemoryArena _arena; + + /// Merge the `dst` layout object with the given `kind`. + /// + /// This more or less ensures that the layout *exists* + /// and has the right kind. + /// + /// If `dst` is null, it will be initialized via `_createLayout`. + /// + /// If `dst` is non-null, it will be checked against `kind`. + /// + LayoutObj* _mergeLayout(LayoutObj*& dst, FossilizedValKind kind); + + /// Merge the `src` layout into the `dst` layout. + /// + /// If `dst` is null, sets it to `src`. + /// + /// If `dst` is non-null, validates that `dst` and + /// `src` have the same kind, and then may recursively + /// merge their contents (e.g., if both are arrays, + /// it will merge the element layouts). + /// + void _mergeLayout(LayoutObj*& dst, LayoutObj* src); + + /// Layout for simple types (integers, strings, etc.) + class SimpleLayoutObj : public LayoutObj + { + public: + SimpleLayoutObj(FossilizedValKind kind, Size size) + : LayoutObj(kind, size, size) + { + } + + SimpleLayoutObj(FossilizedValKind kind) + : LayoutObj(kind) + { + } + }; + + /// Layouts for objects that have one conceptual type parameter. + /// + /// The obvious cases include pointers, arrays, and optionals. + /// + /// This is also used for dictionaries (the element type is + /// a pair). + /// + /// This is also used for variants (the element type is the type + /// of data that a *particular* variant used, whether or not + /// it matches any others). + /// + class ContainerLayoutObj : public LayoutObj + { + public: + ContainerLayoutObj( + FossilizedValKind kind, + LayoutObj* baseLayout, + Size size = 0, + Size alignment = 1) + : LayoutObj(kind, size, alignment), baseLayout(baseLayout) + { + } + + LayoutObj* baseLayout = nullptr; + }; + + /// Layouts for tuples and structs. + /// + class RecordLayoutObj : public LayoutObj + { + public: + RecordLayoutObj(FossilizedValKind kind) + : LayoutObj(kind) + { + } + + struct FieldInfo + { + LayoutObj* layout = nullptr; + Size offset = 0; + }; + + Count fieldCount = 0; + Count fieldCapacity = 0; + FieldInfo* fields = nullptr; + }; + + /// Get or add a field to the given `recordLayout` at the given `index`. + /// + /// If there is not already a field at `index`, then `index` must be + /// equal to the number of existing fields. + /// + RecordLayoutObj::FieldInfo& _getOrAddField(RecordLayoutObj* layout, Index index); + + // The serialized representation only references layouts as part of + // its encoding of variants, with each variant having a prefix field + // that is a relative pointer to its serialized layout. + // + // Because we want to deduplicate layouts, we keep track of all of + // the variant values we have serialized (each of which should be its + // own chunk), and use that array to come back later and write out + // their final layouts (after deduplication). + + struct VariantInfo + { + LayoutObj* layout = nullptr; + ChunkBuilder* chunk = nullptr; + }; + List<VariantInfo> _variants; + + /// Create a chunk to represent `layout`, or return a pre-existing one. + ChunkBuilder* _getOrCreateChunkForLayout(LayoutObj* layout); + + /// Key for deduplication of `LayoutObj`s. + struct LayoutObjKey + { + LayoutObjKey() {} + + LayoutObjKey(LayoutObj* obj) + : obj(obj) + { + } + + LayoutObj* obj = nullptr; + + bool operator==(LayoutObjKey const& that) const; + bool operator!=(LayoutObjKey const& that) const; + + HashCode64 getHashCode() const; + void hashInto(Hasher& hasher) const; + }; + Dictionary<LayoutObjKey, ChunkBuilder*> _mapLayoutObjToChunk; + + // We also go ahead and deduplicate strings as part of serialization, + // since it is easy to do so. + + Dictionary<String, ChunkBuilder*> _mapStringToChunk; + + // Like almost any implementation of `ISerializer`, we need to track + // information on the objects that have been encountered on the other + // side of pointers, so that we can delay serializing their contents + // until an appropriate time. + + struct FossilizedObjectInfo + { + /// Pointer to the "live" object. + void* liveObjectPtr = nullptr; + + /// Chunk that will store the bytes of the fossilized object. + ChunkBuilder* chunk = nullptr; + + /// Layout for a pointer to the fossilized `chunk`. + LayoutObj* ptrLayout = nullptr; + + /// Callback information used by the ISerializer interface. + Callback callback = nullptr; + void* userData = nullptr; + }; + + List<FossilizedObjectInfo*> _fossilizedObjects; + Dictionary<void*, FossilizedObjectInfo*> _mapLiveObjectPtrToFossilizedObject; + Index _writtenObjectDefinitionCount = 0; + + /// Flush all pending operations. + /// + /// This function ensures that all of the to-be-writen objects have + /// been written out, and that all of the variants that need a pointer + /// to a serialized layout get one. + /// + void _flush(); + + // + // As the user makes various begin/end calls on this `SerialWriter`, + // we need to push/pop state information so that we don't lose it. + // + + struct State + { + /// The layout for the value being composed. + LayoutObj* layout = nullptr; + + /// The number of elements/fields or other sub-values written so var. + Count elementCount = 0; + + /// The chunk that holds the data for the value. + /// + /// Can be null if nothing has been written yet, in which + /// case it may be allocated on teh first write. + /// + ChunkBuilder* chunk = nullptr; + + State() {} + + State(LayoutObj* layout, ChunkBuilder* chunk = nullptr) + : layout(layout), chunk(chunk) + { + } + }; + + /// The current state. + State _state; + + /// Stack of suspended states. + List<State> _stack; + + /// The underlying blob builder that we are writing to. + BlobBuilder* _blobBuilder = nullptr; + + // + // Depending on the kind of value being written, it may + // require a different representation. The `Val + + /// Represents a conceptual value to be written. + /// + /// Depending on the kind of value being written, it may + /// require a different representation. The `ValInfo` type + /// abstracts over these differences. + /// + /// Simple values that just consist of bytes can use the + /// `RawData` case. + /// + /// Values that are encoded as a relative pointer use the + /// `RelativePtr` case (unsurprisingly). + /// + /// The `ContentsOfChunk` case is used when the conceptual + /// value is some kind of aggregate that is stored inline + /// rather than indirectly. + /// + struct ValInfo + { + public: + enum class Kind + { + RawData, + RelativePtr, + ContentsOfChunk, + }; + + static ValInfo rawData(void const* data, Size size, Size alignment); + static ValInfo relativePtrTo(ChunkBuilder* targetChunk); + static ValInfo contentsOf(ChunkBuilder* chunk); + + Size getAlignment() const; + + Kind kind; + union + { + struct + { + void const* ptr; + Size size; + Size alignment; + } data; + ChunkBuilder* chunk; + }; + + private: + ValInfo() = default; + ValInfo(const ValInfo&) = default; + ValInfo(ValInfo&&) = default; + ValInfo(Kind kind) + : kind(kind) + { + } + }; + + // In order to allow building up layout information as values are + // being written, the process of writing a value is broken into + // two parts: + // + // * First, the code conceptually "reserves" a destination for the + // value it will write, passing in what it knows about the expected + // layout for the value. The reserve operation returns a layout + // to use (which may be a pre-existing one). + // + // * Second, once the value is ready as a `ValInfo`, the code "commits" + // the write and puts actual data in a chunk somewhere. + // + // For simple values these operations occur on after the other in + // the same function. For complex things that need a begin/end pair, + // the reserve usually happens in a `begin*()` or `push*()` function, + // while the commit happens in an `end*()` or `pop*()` function. + + LayoutObj*& _reserveDestinationForWrite(); + LayoutObj* _reserveDestinationForWrite(FossilizedValKind srcKind); + LayoutObj* _reserveDestinationForWrite(LayoutObj* srcLayout); + + void _commitWrite(ValInfo const& val); + + /// Write a value without doing any of the checks that `_commitWrite` does. + /// + /// (Usually this is called because `_commitWrite()` has already been called) + void _writeValueRaw(ValInfo const& val); + + /// Ensure that the current `State` has a non-null chunk that data + /// can be written to. + /// + void _ensureChunkExists(); + + // There are various different categories of values that each + // need slightly different handling, so each gets its own + // operations that the various `ISerializer::begin()/end()` + // functions will delegate to. + // + // The easiest case is simple values that consist of nothing + // but plain data and have a layout that can be fully summarized + // by the kind. + + void _writeSimpleValue(FossilizedValKind kind, void const* data, size_t size, size_t alignment); + + template<typename T> + void _writeSimpleValue(FossilizedValKind kind, T const& value) + { + _writeSimpleValue(kind, &value, sizeof(value), sizeof(value)); + } + + /// Write a null (relative) pointer. + /// + /// Use this case when there is no more refined type information + /// available about what the layout of the pointed-to data *would* + /// be if the pointer were non-null. + /// + void _writeNull(); + + // + // "Inline" values are aggregates like tuple and structs that + // are always stored by-value in their parent. + // + + void _pushInlineValueScope(FossilizedValKind kind); + void _popInlineValueScope(); + + // + // "Indirect" values are those like optionals that are + // stored as a pointer to an (optional) out-of-line value. + // + + void _pushIndirectValueScope(FossilizedValKind kind); + void _popIndirectValueScope(); + + // + // Many cases of values are *potentially* indirect, in that + // they should be stored via pointer indirection *unless* + // their immediate parent is something that already introduced + // an indirection. + // + // A simple example is a string. A string will by default + // be stored as a (relative) pointer to its content. However, + // if there happens to be an *optional* string, then there is + // no need for a second indirection. + // + // Arrays, dictionaries, strings, and variants are all + // potentially-indirect values. + // + // TODO: This is one aspect of the current design that may need + // to be revisited, if it proves to add too much complexity. + // + + void _pushPotentiallyIndirectValueScope(FossilizedValKind kind); + ChunkBuilder* _popPotentiallyIndirectValueScope(); + + /// Determine if a potentially-indirect value of `kind` should be + /// emitted indirectly, in the current state. + /// + bool _shouldEmitWithPointerIndirection(FossilizedValKind kind); + + /// Helper function to share details between `_popIndirectValueScope` + /// and `_popPotentiallyIndirectValueScope`. + /// + ChunkBuilder* _writeKnownIndirectValueSharedLogic(ChunkBuilder* valueChunk); + + // + // Containers like arrays and dictionaries are potentially-indirect + // values where the chunk that stores their content needs to + // be given a prefix with the element count. + // + + void _pushContainerScope(FossilizedValKind kind); + void _popContainerScope(); + + // + // A variant is a potentially-indirect value where the chunk + // that stores its content needs to be given a prefix with + // the layout of the content. + // + + void _pushVariantScope(); + void _popVariantScope(); + + // + // All of the above operations ultimately bottleneck through + // `_pushState()`/`_popState()`. + // + + void _pushState(LayoutObj* layout); + void _popState(); + +private: + // + // The following declarations are the requirements + // of the `ISerializerImpl` interface: + // + + virtual SerializationMode getMode() override; + + virtual void handleBool(bool& value) override; + + virtual void handleInt8(int8_t& value) override; + virtual void handleInt16(int16_t& value) override; + virtual void handleInt32(Int32& value) override; + virtual void handleInt64(Int64& value) override; + + virtual void handleUInt8(uint8_t& value) override; + virtual void handleUInt16(uint16_t& value) override; + virtual void handleUInt32(UInt32& value) override; + virtual void handleUInt64(UInt64& value) override; + + virtual void handleFloat32(float& value) override; + virtual void handleFloat64(double& value) override; + + virtual void handleString(String& value) override; + + virtual void beginArray() override; + virtual void endArray() override; + + virtual void beginOptional() override; + virtual void endOptional() override; + + virtual void beginDictionary() override; + virtual void endDictionary() override; + + virtual bool hasElements() override; + + virtual void beginTuple() override; + virtual void endTuple() override; + + virtual void beginStruct() override; + virtual void endStruct() override; + + virtual void beginVariant() override; + virtual void endVariant() override; + + virtual void handleFieldKey(char const* name, Int index) override; + + virtual void handleSharedPtr(void*& value, Callback callback, void* userData) override; + virtual void handleUniquePtr(void*& value, Callback callback, void* userData) override; + + virtual void handleDeferredObjectContents(void* valuePtr, Callback callback, void* userData) + override; +}; + +/// Serializer implementation for reading objects from a fossil-format blob. +struct SerialReader : ISerializerImpl +{ +public: + SerialReader(FossilizedValRef valRef); + ~SerialReader(); + +private: + /// A state that the reader can be in. + struct State + { + /// Type of state; related to the kind of value being read from. + /// + enum class Type + { + Root, + Array, + Dictionary, + Optional, + Tuple, + Struct, + Object, + }; + + /// The type of state. + Type type = Type::Root; + + /// The fossilized value (data and layout) that is being read from. + /// + /// Depending on the `type` of state, this might either be the next value + /// that will be read (e.g., for the `Root` case), or it might be + /// a container that is a parent of the next value to be read. + /// + FossilizedValRef baseValue; + + /// Index of next element to read. + /// + /// This is used in the case where `baseValue` is some kind of + /// container or record. + /// + Index elementIndex = 0; + + /// Total number of values that can be read. + /// + /// If `baseValue` is a container, this is the element count. + /// If `baseValue` is a tuple/struct, this is the field count. + /// If `baseValue` is an optional, this is either zero or one. + /// If this state is a singleton case like `Root`, will be one. + /// + Count elementCount = 0; + }; + + /// The current state. + State _state; + + /// Stack of saved states. + List<State> _stack; + + void _pushState(); + void _popState(); + + // + // Like other `ISerializerImpl`s for reading, we track objects + // that are in the process of being read in, to avoid possible + // unbounded recursion (and detect circularities when they + // occur). + // + + enum class ObjectState + { + Unread, + ReadingInProgress, + ReadingComplete, + }; + struct ObjectInfo : public RefObject + { + ObjectState state = ObjectState::Unread; + + void* resurrectedObjectPtr = nullptr; + FossilizedValRef fossilizedObjectRef; + }; + Dictionary<void*, RefPtr<ObjectInfo>> _mapFossilizedObjectPtrToObjectInfo; + + // + // Again, like other `ISerializerImpl`s for reading, we + // maintain a list of deferred serialization actions that + // need to be performed to finish reading the state of + // in-memory objects. + // + + struct DeferredAction + { + void* resurrectedObjectPtr; + + State savedState; + + Callback callback; + void* userData; + }; + List<DeferredAction> _deferredActions; + + /// Execute all deferred actions that are still pending. + void _flush(); + + /// Read a simple/inline value. + /// + /// This is the case for scalars, tuples, and structs. + /// + FossilizedValRef _readValRef(); + + /// Read an indirect value. + /// + /// This is the case for things like optionals, that are + /// always encoded as a pointer. + /// + FossilizedValRef _readIndirectValRef(); + + /// Read a potentially-indirect value. + /// + /// If the value that gets read is a pointer, then this + /// function will return a reference to whatever it points to. + /// + /// Otherwise, this will return a reference to the value itself. + /// + FossilizedValRef _readPotentiallyIndirectValRef(); + +private: + // + // The following declarations are the requirements + // of the `ISerializerImpl` interface: + // + + virtual SerializationMode getMode() override; + + virtual void handleBool(bool& value) override; + + virtual void handleInt8(int8_t& value) override; + virtual void handleInt16(int16_t& value) override; + virtual void handleInt32(Int32& value) override; + virtual void handleInt64(Int64& value) override; + + virtual void handleUInt8(uint8_t& value) override; + virtual void handleUInt16(uint16_t& value) override; + virtual void handleUInt32(UInt32& value) override; + virtual void handleUInt64(UInt64& value) override; + + virtual void handleFloat32(float& value) override; + virtual void handleFloat64(double& value) override; + + virtual void handleString(String& value) override; + + virtual void beginArray() override; + virtual void endArray() override; + + virtual void beginDictionary() override; + virtual void endDictionary() override; + + virtual bool hasElements() override; + + virtual void beginStruct() override; + virtual void endStruct() override; + + virtual void beginVariant() override; + virtual void endVariant() override; + + virtual void handleFieldKey(char const* name, Int index) override; + + virtual void beginTuple() override; + virtual void endTuple() override; + + virtual void beginOptional() override; + virtual void endOptional() override; + + virtual void handleSharedPtr(void*& value, Callback callback, void* userData) override; + virtual void handleUniquePtr(void*& value, Callback callback, void* userData) override; + + virtual void handleDeferredObjectContents(void* valuePtr, Callback callback, void* userData) + override; +}; + +} // namespace Fossil +} // namespace Slang + +#endif diff --git a/source/slang/slang-serialize-riff.cpp b/source/slang/slang-serialize-riff.cpp index 01b39e825..469803c2a 100644 --- a/source/slang/slang-serialize-riff.cpp +++ b/source/slang/slang-serialize-riff.cpp @@ -174,6 +174,21 @@ void RIFFSerialWriter::beginStruct() _cursor.beginListChunk(RIFFSerial::kStructFourCC); } +void RIFFSerialWriter::endStruct() +{ + _cursor.endChunk(); +} + +void RIFFSerialWriter::beginVariant() +{ + beginStruct(); +} + +void RIFFSerialWriter::endVariant() +{ + endStruct(); +} + void RIFFSerialWriter::handleFieldKey(char const* name, Int index) { // For now we are ignoring field keys, and treating @@ -182,11 +197,6 @@ void RIFFSerialWriter::handleFieldKey(char const* name, Int index) SLANG_UNUSED(index); } -void RIFFSerialWriter::endStruct() -{ - _cursor.endChunk(); -} - void RIFFSerialWriter::beginTuple() { _cursor.beginListChunk(RIFFSerial::kTupleFourCC); @@ -506,6 +516,21 @@ void RIFFSerialReader::beginStruct() _beginListChunk(RIFFSerial::kStructFourCC); } +void RIFFSerialReader::endStruct() +{ + _endListChunk(); +} + +void RIFFSerialReader::beginVariant() +{ + beginStruct(); +} + +void RIFFSerialReader::endVariant() +{ + endStruct(); +} + void RIFFSerialReader::handleFieldKey(char const* name, Int index) { // For now we are ignoring field keys, and treating @@ -514,11 +539,6 @@ void RIFFSerialReader::handleFieldKey(char const* name, Int index) SLANG_UNUSED(index); } -void RIFFSerialReader::endStruct() -{ - _endListChunk(); -} - void RIFFSerialReader::beginTuple() { _beginListChunk(RIFFSerial::kTupleFourCC); diff --git a/source/slang/slang-serialize-riff.h b/source/slang/slang-serialize-riff.h index a464a5ded..87f83f3f0 100644 --- a/source/slang/slang-serialize-riff.h +++ b/source/slang/slang-serialize-riff.h @@ -224,9 +224,13 @@ private: virtual bool hasElements() override; virtual void beginStruct() override; - virtual void handleFieldKey(char const* name, Int index) override; virtual void endStruct() override; + virtual void beginVariant() override; + virtual void endVariant() override; + + virtual void handleFieldKey(char const* name, Int index) override; + virtual void beginTuple() override; virtual void endTuple() override; @@ -410,9 +414,13 @@ private: virtual bool hasElements() override; virtual void beginStruct() override; - virtual void handleFieldKey(char const* name, Int index) override; virtual void endStruct() override; + virtual void beginVariant() override; + virtual void endVariant() override; + + virtual void handleFieldKey(char const* name, Int index) override; + virtual void beginTuple() override; virtual void endTuple() override; diff --git a/source/slang/slang-serialize.h b/source/slang/slang-serialize.h index d76ab8338..b962ee2b7 100644 --- a/source/slang/slang-serialize.h +++ b/source/slang/slang-serialize.h @@ -335,6 +335,30 @@ struct ISerializerImpl /// End serializing a struct value. virtual void endStruct() = 0; + /// Begin serializing a variant value. + /// + /// A variant should be used to serialize any type + /// that behaves like a "tagged union," where different + /// instances may have different sequences of members, + /// of different types. + /// + /// User code reading from a variant must be able to + /// use the members read so far to determine what + /// members it should read next (e.g., by serializing + /// a tag enumerant first, followed by the tag-dependent + /// members). + /// + /// A variant is otherwise like a struct. Some serializer + /// implementations may treat variants just like structs, + /// while others may rely on any type serialized as a + /// struct always including the same members in the same + /// order. + /// + virtual void beginVariant() = 0; + + /// End serializing a variant value. + virtual void endVariant() = 0; + /// Set the key for the next struct field to be serialized. /// /// If no name is available for the field, `name` may be `nullptr`. @@ -623,6 +647,21 @@ private: Serializer _serializer; }; +struct ScopedSerializerVariant +{ +public: + ScopedSerializerVariant(Serializer const& serializer) + : _serializer(serializer) + { + serializer->beginVariant(); + } + + ~ScopedSerializerVariant() { _serializer->endVariant(); } + +private: + Serializer _serializer; +}; + struct ScopedSerializerTuple { public: @@ -667,8 +706,8 @@ private: #define SLANG_SCOPED_SERIALIZER_STRUCT(SERIALIZER) \ ::Slang::ScopedSerializerStruct SLANG_CONCAT(_scopedSerializerStruct, __LINE__)(SERIALIZER) -#define SLANG_SCOPED_SERIALIZER_TAGGED_UNION(SERIALIZER) \ - ::Slang::ScopedSerializerStruct SLANG_CONCAT(_scopedSerializerStruct, __LINE__)(SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_VARIANT(SERIALIZER) \ + ::Slang::ScopedSerializerVariant SLANG_CONCAT(_scopedSerializerVariant, __LINE__)(SERIALIZER) #define SLANG_SCOPED_SERIALIZER_TUPLE(SERIALIZER) \ ::Slang::ScopedSerializerTuple SLANG_CONCAT(_scopedSerializerTuple, __LINE__)(SERIALIZER) |
