// 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(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` // 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(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(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(elementLayoutChunk); } break; case FossilizedValKind::Array: case FossilizedValKind::Dictionary: { auto containerLayout = (ContainerLayoutObj*)layout; auto elementLayout = containerLayout->baseLayout; auto elementLayoutChunk = _getOrCreateChunkForLayout(elementLayout); chunk->writeRelativePtr(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(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(valRef)->getValue(); } void SerialReader::handleInt8(int8_t& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleInt16(int16_t& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleInt32(Int32& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleInt64(Int64& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleUInt8(uint8_t& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleUInt16(uint16_t& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleUInt32(UInt32& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleUInt64(UInt64& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleFloat32(float& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleFloat64(double& value) { auto valRef = _readValRef(); value = as(valRef)->getValue(); } void SerialReader::handleString(String& value) { auto valRef = _readPotentiallyIndirectValRef(); if (!valRef) { value = String(); } else { value = as(valRef)->getValue(); } } void SerialReader::beginArray() { auto valRef = _readPotentiallyIndirectValRef(); auto arrayRef = as(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(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(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(valRef); auto contentValRef = getVariantContent(variantRef); auto contentRecordRef = as(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(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(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(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(_state.baseValue); return getField(recordRef, index); } case State::Type::Optional: { SLANG_ASSERT(_state.elementCount == 1); SLANG_ASSERT(_state.elementIndex == 0); auto optionalRef = as(_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(_state.baseValue); return getElement(containerRef, index); } default: SLANG_UNEXPECTED("unhandled case"); break; } } FossilizedValRef SerialReader::_readIndirectValRef() { auto ptrValRef = _readValRef(); auto ptrRef = as(ptrValRef); auto valRef = getPtrTarget(ptrRef); return valRef; } FossilizedValRef SerialReader::_readPotentiallyIndirectValRef() { auto valRef = _readValRef(); if (auto ptrRef = as(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