diff --git a/c++/src/capnp/encoding-test.c++ b/c++/src/capnp/encoding-test.c++ index a7a01ab982bc5cc120019b2cc0e3a9772c9ad9c6..e5c562951e268651c5e99ddf3f31a98740f7889b 100644 --- a/c++/src/capnp/encoding-test.c++ +++ b/c++/src/capnp/encoding-test.c++ @@ -1747,6 +1747,51 @@ TEST(Encoding, GlobalConstants) { } } +TEST(Encoding, HasEmptyStruct) { + MallocMessageBuilder message; + auto root = message.initRoot<test::TestObject>(); + + EXPECT_EQ(1, root.totalSizeInWords()); + + EXPECT_FALSE(root.asReader().hasObjectField()); + EXPECT_FALSE(root.hasObjectField()); + root.initObjectField<test::TestEmptyStruct>(); + EXPECT_TRUE(root.asReader().hasObjectField()); + EXPECT_TRUE(root.hasObjectField()); + + EXPECT_EQ(1, root.totalSizeInWords()); +} + +TEST(Encoding, HasEmptyList) { + MallocMessageBuilder message; + auto root = message.initRoot<test::TestObject>(); + + EXPECT_EQ(1, root.totalSizeInWords()); + + EXPECT_FALSE(root.asReader().hasObjectField()); + EXPECT_FALSE(root.hasObjectField()); + root.initObjectField<List<int32_t>>(0); + EXPECT_TRUE(root.asReader().hasObjectField()); + EXPECT_TRUE(root.hasObjectField()); + + EXPECT_EQ(1, root.totalSizeInWords()); +} + +TEST(Encoding, HasEmptyStructList) { + MallocMessageBuilder message; + auto root = message.initRoot<test::TestObject>(); + + EXPECT_EQ(1, root.totalSizeInWords()); + + EXPECT_FALSE(root.asReader().hasObjectField()); + EXPECT_FALSE(root.hasObjectField()); + root.initObjectField<List<TestAllTypes>>(0); + EXPECT_TRUE(root.asReader().hasObjectField()); + EXPECT_TRUE(root.hasObjectField()); + + EXPECT_EQ(2, root.totalSizeInWords()); +} + } // namespace } // namespace _ (private) } // namespace capnp diff --git a/c++/src/capnp/layout.c++ b/c++/src/capnp/layout.c++ index 8bdfcff891d0b71262090845db0ce0f809a54800..6b77d68d5ab2909bd0d6ffeabf4c1dbab2869f08 100644 --- a/c++/src/capnp/layout.c++ +++ b/c++/src/capnp/layout.c++ @@ -86,12 +86,43 @@ struct WirePointer { return reinterpret_cast<const word*>(this) + 1 + (static_cast<int32_t>(offsetAndKind.get()) >> 2); } - KJ_ALWAYS_INLINE(void setKindAndTarget(Kind kind, word* target)) { + KJ_ALWAYS_INLINE(void setKindAndTarget(Kind kind, word* target, SegmentBuilder* segment)) { + // Check that the target is really in the same segment, otherwise subtracting pointers is + // undefined behavior. As it turns out, it's undefined behavior that actually produces + // unexpected results in a real-world situation that actually happened: At one time, + // OrphanBuilder's "tag" (a WirePointer) was allowed to be initialized as if it lived in + // a particular segment when in fact it does not. On 32-bit systems, where words might + // only be 32-bit aligned, it's possible that the difference between `this` and `target` is + // not a whole number of words. But clang optimizes: + // (target - (word*)this - 1) << 2 + // to: + // (((ptrdiff_t)target - (ptrdiff_t)this - 8) >> 1) + // So now when the pointers are not aligned the same, we can end up corrupting the bottom + // two bits, where `kind` is stored. For example, this turns a struct into a far pointer. + // Ouch! + KJ_DREQUIRE(segment->containsInterval( + reinterpret_cast<word*>(this), reinterpret_cast<word*>(this + 1))); + KJ_DREQUIRE(segment->containsInterval(target, target)); offsetAndKind.set(((target - reinterpret_cast<word*>(this) - 1) << 2) | kind); } KJ_ALWAYS_INLINE(void setKindWithZeroOffset(Kind kind)) { offsetAndKind.set(kind); } + KJ_ALWAYS_INLINE(void setKindAndTargetForEmptyStruct()) { + // This pointer points at an empty struct. Assuming the WirePointer itself is in-bounds, we + // can set the target to point either at the WirePointer itself or immediately after it. The + // latter would cause the WirePointer to be "null" (since for an empty struct the upper 32 + // bits are going to be zero). So we set an offset of -1, as if the struct were allocated + // immediately before this pointer, to distinguish it from null. + offsetAndKind.set(0xfffffffc); + } + KJ_ALWAYS_INLINE(void setKindForOrphan(Kind kind)) { + // OrphanBuilder contains a WirePointer, but since it isn't located in a segment, it should + // not have a valid offset (unless it is a FAR pointer). We set its offset to -1 because + // setting it to zero would mean a pointer to an empty struct would appear to be a null pointer. + KJ_DREQUIRE(kind != FAR); + offsetAndKind.set(kind | 0xfffffffc); + } KJ_ALWAYS_INLINE(ElementCount inlineCompositeListElementCount() const) { return (offsetAndKind.get() >> 2) * ELEMENTS; @@ -272,23 +303,16 @@ struct WireHelpers { // segment belonging to the arena. `ref` will be initialized as a non-far pointer, but its // target offset will be set to zero. - if (amount == 0 * WORDS && kind == WirePointer::STRUCT) { - // Allocating a zero-sized struct. If it happens to be allocated in the space immediately - // after the pointer, we'll have a problem: the pointer will end up all-zero, so isNull() - // will be true. This can lead to all kinds of weird behavior later on. Since the target - // has zero-size anyway, we can really set the pointer to point anywhere, as long as it - // is in-bounds. So, we can have the pointer point back at itself (an offset of -1). This - // should be exceedingly rare in practice since empty structs are pretty useless. - // - // Note that the check for kind == WirePointer::STRUCT will hopefully cause this whole branch - // to be optimized away from all the call sites that are allocating non-structs. - ref->setKindAndTarget(WirePointer::STRUCT, reinterpret_cast<word*>(ref)); - return reinterpret_cast<word*>(ref); - } - if (orphanArena == nullptr) { if (!ref->isNull()) zeroObject(segment, ref); + if (amount == 0 * WORDS && kind == WirePointer::STRUCT) { + // Note that the check for kind == WirePointer::STRUCT will hopefully cause this whole + // branch to be optimized away from all the call sites that are allocating non-structs. + ref->setKindAndTargetForEmptyStruct(); + return reinterpret_cast<word*>(ref); + } + word* ptr = segment->allocate(amount); if (ptr == nullptr) { @@ -306,19 +330,20 @@ struct WireHelpers { // Initialize the landing pad to indicate that the data immediately follows the pad. ref = reinterpret_cast<WirePointer*>(ptr); - ref->setKindAndTarget(kind, ptr + POINTER_SIZE_IN_WORDS); + ref->setKindAndTarget(kind, ptr + POINTER_SIZE_IN_WORDS, segment); // Allocated space follows new pointer. return ptr + POINTER_SIZE_IN_WORDS; } else { - ref->setKindAndTarget(kind, ptr); + ref->setKindAndTarget(kind, ptr, segment); return ptr; } } else { // orphanArena is non-null. Allocate an orphan. + KJ_DASSERT(ref->isNull()); auto allocation = orphanArena->allocate(amount); segment = allocation.segment; - ref->setKindWithZeroOffset(kind); + ref->setKindForOrphan(kind); return allocation.words; } } @@ -790,7 +815,7 @@ struct WireHelpers { if (dstSegment == srcSegment) { // Same segment, so create a direct pointer. - dst->setKindAndTarget(srcTag->kind(), srcPtr); + dst->setKindAndTarget(srcTag->kind(), srcPtr, dstSegment); // We can just copy the upper 32 bits. (Use memcpy() to comply with aliasing rules.) memcpy(&dst->upper32Bits, &srcTag->upper32Bits, sizeof(srcTag->upper32Bits)); @@ -816,7 +841,7 @@ struct WireHelpers { dst->farRef.set(farSegment->getSegmentId()); } else { // Simple landing pad is just a pointer. - landingPad->setKindAndTarget(srcTag->kind(), srcPtr); + landingPad->setKindAndTarget(srcTag->kind(), srcPtr, srcSegment); memcpy(&landingPad->upper32Bits, &srcTag->upper32Bits, sizeof(srcTag->upper32Bits)); dst->setFar(false, srcSegment->getOffsetTo(reinterpret_cast<word*>(landingPad))); @@ -853,7 +878,7 @@ struct WireHelpers { useDefault: if (defaultValue == nullptr || reinterpret_cast<const WirePointer*>(defaultValue)->isNull()) { - return initStructPointer(ref, segment, size); + return initStructPointer(ref, segment, size, orphanArena); } refTarget = copyMessage(segment, ref, reinterpret_cast<const WirePointer*>(defaultValue)); defaultValue = nullptr; // If the default value is itself invalid, don't use it again. @@ -942,7 +967,8 @@ struct WireHelpers { StructSize elementSize, BuilderArena* orphanArena = nullptr)) { if (elementSize.preferredListEncoding != FieldSize::INLINE_COMPOSITE) { // Small data-only struct. Allocate a list of primitives instead. - return initListPointer(ref, segment, elementCount, elementSize.preferredListEncoding); + return initListPointer(ref, segment, elementCount, elementSize.preferredListEncoding, + orphanArena); } auto wordsPerElement = elementSize.total() / ELEMENTS; @@ -976,7 +1002,7 @@ struct WireHelpers { static KJ_ALWAYS_INLINE(ListBuilder getWritableListPointer( WirePointer* origRef, word* origRefTarget, SegmentBuilder* origSegment, FieldSize elementSize, - const word* defaultValue)) { + const word* defaultValue, BuilderArena* orphanArena = nullptr)) { KJ_DREQUIRE(elementSize != FieldSize::INLINE_COMPOSITE, "Use getStructList{Element,Field}() for structs."); @@ -1614,7 +1640,7 @@ struct WireHelpers { zeroObject(segment, ref); } - if (value.segment == nullptr) { + if (value == nullptr) { // Set null. memset(ref, 0, sizeof(*ref)); } else if (value.tagAsPtr()->kind() == WirePointer::FAR) { @@ -1631,14 +1657,17 @@ struct WireHelpers { } static OrphanBuilder disown(SegmentBuilder* segment, WirePointer* ref) { - if (ref->isNull()) { - return OrphanBuilder(); - } else { - OrphanBuilder result(ref, segment, - ref->kind() == WirePointer::FAR ? nullptr : ref->target()); - memset(ref, 0, sizeof(*ref)); - return result; + OrphanBuilder result(ref, segment, + getWritableObjectPointer(segment, ref, nullptr).getLocation()); + + if (!ref->isNull() && ref->kind() != WirePointer::FAR) { + result.tagAsPtr()->setKindForOrphan(ref->kind()); } + + // Zero out the pointer that was disowned. + memset(ref, 0, sizeof(*ref)); + + return result; } // ----------------------------------------------------------------- @@ -2589,7 +2618,7 @@ OrphanBuilder OrphanBuilder::initStruct(BuilderArena* arena, StructSize size) { OrphanBuilder result; StructBuilder builder = WireHelpers::initStructPointer(result.tagAsPtr(), nullptr, size, arena); result.segment = builder.segment; - result.location = reinterpret_cast<word*>(builder.data); + result.location = builder.getLocation(); return result; } @@ -2599,23 +2628,18 @@ OrphanBuilder OrphanBuilder::initList( ListBuilder builder = WireHelpers::initListPointer( result.tagAsPtr(), nullptr, elementCount, elementSize, arena); result.segment = builder.segment; - result.location = reinterpret_cast<word*>(builder.ptr); + result.location = builder.getLocation(); return result; } OrphanBuilder OrphanBuilder::initStructList( BuilderArena* arena, ElementCount elementCount, StructSize elementSize) { - if (elementSize.preferredListEncoding != FieldSize::INLINE_COMPOSITE) { - // Small data-only struct. Allocate a list of primitives instead. - return initList(arena, elementCount, elementSize.preferredListEncoding); - } else { - OrphanBuilder result; - ListBuilder builder = WireHelpers::initStructListPointer( - result.tagAsPtr(), nullptr, elementCount, elementSize, arena); - result.segment = builder.segment; - result.location = reinterpret_cast<word*>(builder.ptr) - POINTER_SIZE_IN_WORDS; - return result; - } + OrphanBuilder result; + ListBuilder builder = WireHelpers::initStructListPointer( + result.tagAsPtr(), nullptr, elementCount, elementSize, arena); + result.segment = builder.segment; + result.location = builder.getLocation(); + return result; } OrphanBuilder OrphanBuilder::initText(BuilderArena* arena, ByteCount size) { @@ -2669,106 +2693,93 @@ OrphanBuilder OrphanBuilder::copy(BuilderArena* arena, Data::Reader copyFrom) { } StructBuilder OrphanBuilder::asStruct(StructSize size) { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); + StructBuilder result = WireHelpers::getWritableStructPointer( - tagAsPtr(), location, segment, size, nullptr); + tagAsPtr(), location, segment, size, nullptr, segment->getArena()); // Watch out, the pointer could have been updated if the object had to be relocated. - if (tagAsPtr()->kind() == WirePointer::FAR) { - location = nullptr; - } else { - location = reinterpret_cast<word*>(result.data); - } + location = reinterpret_cast<word*>(result.data); return result; } ListBuilder OrphanBuilder::asList(FieldSize elementSize) { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); + ListBuilder result = WireHelpers::getWritableListPointer( - tagAsPtr(), location, segment, elementSize, nullptr); + tagAsPtr(), location, segment, elementSize, nullptr, segment->getArena()); // Watch out, the pointer could have been updated if the object had to be relocated. - if (tagAsPtr()->kind() == WirePointer::FAR) { - location = nullptr; - } else { - location = reinterpret_cast<word*>(result.ptr); - } + // (Actually, currently this is not true for primitive lists, but let's not turn into a bug if + // it changes!) + location = result.getLocation(); return result; } ListBuilder OrphanBuilder::asStructList(StructSize elementSize) { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); + ListBuilder result = WireHelpers::getWritableStructListPointer( - tagAsPtr(), location, segment, elementSize, nullptr); + tagAsPtr(), location, segment, elementSize, nullptr, segment->getArena()); // Watch out, the pointer could have been updated if the object had to be relocated. - if (tagAsPtr()->kind() == WirePointer::FAR) { - location = nullptr; - } else if (result.step * ELEMENTS <= BITS_PER_WORD * WORDS) { - location = reinterpret_cast<word*>(result.ptr); - } else { - location = reinterpret_cast<word*>(result.ptr) - POINTER_SIZE_IN_WORDS; - } + location = result.getLocation(); return result; } Text::Builder OrphanBuilder::asText() { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); + // Never relocates. return WireHelpers::getWritableTextPointer(tagAsPtr(), location, segment, nullptr, 0 * BYTES); } Data::Builder OrphanBuilder::asData() { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); + // Never relocates. return WireHelpers::getWritableDataPointer(tagAsPtr(), location, segment, nullptr, 0 * BYTES); } ObjectBuilder OrphanBuilder::asObject() { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); + ObjectBuilder result = WireHelpers::getWritableObjectPointer( segment, tagAsPtr(), location, nullptr); // Watch out, the pointer could have been updated if the object had to be relocated. - if (tagAsPtr()->kind() == WirePointer::FAR) { - location = nullptr; - } else { - switch (result.kind) { - case ObjectKind::STRUCT: - location = reinterpret_cast<word*>(result.structBuilder.data); - break; - case ObjectKind::LIST: - if (tagAsPtr()->listRef.elementSize() == FieldSize::INLINE_COMPOSITE) { - location = reinterpret_cast<word*>(result.listBuilder.ptr) - POINTER_SIZE_IN_WORDS; - } else { - location = reinterpret_cast<word*>(result.listBuilder.ptr); - } - break; - case ObjectKind::NULL_POINTER: - location = nullptr; - break; - } - } + location = result.getLocation(); return result; } StructReader OrphanBuilder::asStructReader(StructSize size) const { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); return WireHelpers::readStructPointer( segment, tagAsPtr(), location, nullptr, std::numeric_limits<int>::max()); } ListReader OrphanBuilder::asListReader(FieldSize elementSize) const { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); return WireHelpers::readListPointer( segment, tagAsPtr(), location, nullptr, elementSize, std::numeric_limits<int>::max()); } Text::Reader OrphanBuilder::asTextReader() const { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); return WireHelpers::readTextPointer(segment, tagAsPtr(), location, nullptr, 0 * BYTES); } Data::Reader OrphanBuilder::asDataReader() const { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); return WireHelpers::readDataPointer(segment, tagAsPtr(), location, nullptr, 0 * BYTES); } ObjectReader OrphanBuilder::asObjectReader() const { + KJ_DASSERT(tagAsPtr()->isNull() == (location == nullptr)); return WireHelpers::readObjectPointer( segment, tagAsPtr(), location, nullptr, std::numeric_limits<int>::max()); } @@ -2777,14 +2788,13 @@ void OrphanBuilder::euthanize() { // Carefully catch any exceptions and rethrow them as recoverable exceptions since we may be in // a destructor. auto exception = kj::runCatchingExceptions([&]() { - auto ref = reinterpret_cast<WirePointer*>(&tag); - if (ref->kind() == WirePointer::FAR) { - WireHelpers::zeroObject(segment, ref); + if (tagAsPtr()->kind() == WirePointer::FAR) { + WireHelpers::zeroObject(segment, tagAsPtr()); } else { - WireHelpers::zeroObject(segment, reinterpret_cast<WirePointer*>(&tag), location); + WireHelpers::zeroObject(segment, tagAsPtr(), location); } - memset(ref, 0, sizeof(*ref)); + memset(&tag, 0, sizeof(tag)); segment = nullptr; location = nullptr; }); diff --git a/c++/src/capnp/layout.h b/c++/src/capnp/layout.h index 5584500beeacd50bc19b27e7ea968ec892370488..5566da8475d97781c6cb51d7b53f900bf625880e 100644 --- a/c++/src/capnp/layout.h +++ b/c++/src/capnp/layout.h @@ -279,6 +279,10 @@ public: static StructBuilder getRoot(SegmentBuilder* segment, word* location, StructSize size); static void adoptRoot(SegmentBuilder* segment, word* location, OrphanBuilder orphan); + inline word* getLocation() { return reinterpret_cast<word*>(data); } + // Get the object's location. Only valid for independently-allocated objects (i.e. not list + // elements). + inline BitCount getDataSectionSize() const { return dataSize; } inline WirePointerCount getPointerSectionSize() const { return pointerCount; } inline Data::Builder getDataSectionAsBlob(); @@ -533,6 +537,17 @@ public: : segment(nullptr), ptr(nullptr), elementCount(0 * ELEMENTS), step(0 * BITS / ELEMENTS) {} + inline word* getLocation() { + // Get the object's location. Only valid for independently-allocated objects (i.e. not list + // elements). + + if (step * ELEMENTS <= BITS_PER_WORD * WORDS) { + return reinterpret_cast<word*>(ptr); + } else { + return reinterpret_cast<word*>(ptr) - POINTER_SIZE_IN_WORDS; + } + } + inline ElementCount size() const; // The number of elements in the list. @@ -722,6 +737,15 @@ struct ObjectBuilder { ObjectBuilder(ListBuilder listBuilder) : kind(ObjectKind::LIST), listBuilder(listBuilder) {} + inline word* getLocation() { + switch (kind) { + case ObjectKind::NULL_POINTER: return nullptr; + case ObjectKind::STRUCT: return structBuilder.getLocation(); + case ObjectKind::LIST: return listBuilder.getLocation(); + } + return nullptr; + } + ObjectReader asReader() const; inline ObjectBuilder(ObjectBuilder& other) { memcpy(this, &other, sizeof(*this)); } @@ -773,8 +797,8 @@ public: OrphanBuilder& operator=(const OrphanBuilder& other) = delete; inline OrphanBuilder& operator=(OrphanBuilder&& other); - inline bool operator==(decltype(nullptr)) const { return segment == nullptr; } - inline bool operator!=(decltype(nullptr)) const { return segment != nullptr; } + inline bool operator==(decltype(nullptr)) const { return location == nullptr; } + inline bool operator!=(decltype(nullptr)) const { return location != nullptr; } StructBuilder asStruct(StructSize size); // Interpret as a struct, or throw an exception if not a struct. @@ -816,8 +840,7 @@ private: // FAR pointer. word* location; - // Pointer to the object. Invalid if the tag is a FAR pointer (in which case you need to follow - // the FAR pointer instead). + // Pointer to the object, or nullptr if the pointer is null. inline OrphanBuilder(const void* tagPtr, SegmentBuilder* segment, word* location) : segment(segment), location(location) { diff --git a/c++/src/capnp/orphan-test.c++ b/c++/src/capnp/orphan-test.c++ index 5cc94fe3b4e281a335a515d74ec7f0b02453bc71..175a746a9da8646beaf6b8eb2bedf6ba61d788f9 100644 --- a/c++/src/capnp/orphan-test.c++ +++ b/c++/src/capnp/orphan-test.c++ @@ -781,12 +781,103 @@ TEST(Orphans, FarPointer) { EXPECT_TRUE(orphan != nullptr); EXPECT_FALSE(orphan == nullptr); - KJ_DBG(orphan != nullptr, orphan == nullptr); - checkTestMessage(orphan.getReader()); checkTestMessage(orphan.get()); } +TEST(Orphans, UpgradeStruct) { + MallocMessageBuilder builder; + auto root = builder.initRoot<test::TestObject>(); + + auto old = root.initObjectField<test::TestOldVersion>(); + old.setOld1(1234); + old.setOld2("foo"); + + auto orphan = root.disownObjectField<test::TestNewVersion>(); + + // Relocation has not occurred yet. + old.setOld1(12345); + EXPECT_EQ(12345, orphan.getReader().getOld1()); + EXPECT_EQ("foo", old.getOld2()); + + // This will relocate the struct. + auto newVersion = orphan.get(); + + EXPECT_EQ(0, old.getOld1()); + EXPECT_EQ("", old.getOld2()); + + EXPECT_EQ(12345, newVersion.getOld1()); + EXPECT_EQ("foo", newVersion.getOld2()); +} + +TEST(Orphans, UpgradeStructList) { + MallocMessageBuilder builder; + auto root = builder.initRoot<test::TestObject>(); + + auto old = root.initObjectField<List<test::TestOldVersion>>(2); + old[0].setOld1(1234); + old[0].setOld2("foo"); + old[1].setOld1(4321); + old[1].setOld2("bar"); + + auto orphan = root.disownObjectField<List<test::TestNewVersion>>(); + + // Relocation has not occurred yet. + old[0].setOld1(12345); + EXPECT_EQ(12345, orphan.getReader()[0].getOld1()); + EXPECT_EQ("foo", old[0].getOld2()); + + // This will relocate the struct. + auto newVersion = orphan.get(); + + EXPECT_EQ(0, old[0].getOld1()); + EXPECT_EQ("", old[0].getOld2()); + + EXPECT_EQ(12345, newVersion[0].getOld1()); + EXPECT_EQ("foo", newVersion[0].getOld2()); + EXPECT_EQ(4321, newVersion[1].getOld1()); + EXPECT_EQ("bar", newVersion[1].getOld2()); +} + +TEST(Orphans, DisownNull) { + MallocMessageBuilder builder; + auto root = builder.initRoot<TestAllTypes>(); + + { + Orphan<TestAllTypes> orphan = root.disownStructField(); + EXPECT_TRUE(orphan == nullptr); + + checkTestMessageAllZero(orphan.getReader()); + EXPECT_TRUE(orphan == nullptr); + + // get()ing the orphan allocates an object, for security reasons. + checkTestMessageAllZero(orphan.get()); + EXPECT_FALSE(orphan == nullptr); + } + + { + Orphan<List<int32_t>> orphan = root.disownInt32List(); + EXPECT_TRUE(orphan == nullptr); + + EXPECT_EQ(0, orphan.getReader().size()); + EXPECT_TRUE(orphan == nullptr); + + EXPECT_EQ(0, orphan.get().size()); + EXPECT_TRUE(orphan == nullptr); + } + + { + Orphan<List<TestAllTypes>> orphan = root.disownStructList(); + EXPECT_TRUE(orphan == nullptr); + + EXPECT_EQ(0, orphan.getReader().size()); + EXPECT_TRUE(orphan == nullptr); + + EXPECT_EQ(0, orphan.get().size()); + EXPECT_TRUE(orphan == nullptr); + } +} + } // namespace } // namespace _ (private) } // namespace capnp diff --git a/c++/src/capnp/orphan.h b/c++/src/capnp/orphan.h index 24a5fcd3db195d160ac3d1003e58afc0ed4d0350..6cb945abc1ce23665f66a90c6c65d29475a2741b 100644 --- a/c++/src/capnp/orphan.h +++ b/c++/src/capnp/orphan.h @@ -53,6 +53,12 @@ public: Orphan& operator=(Orphan&&) = default; inline typename T::Builder get(); + // Get the underlying builder. If the orphan is null, this will allocate and return a default + // object rather than crash. This is done for security -- otherwise, you might enable a DoS + // attack any time you disown a field and fail to check if it is null. In the case of structs, + // this means that the orphan is no longer null after get() returns. In the case of lists, + // no actual object is allocated since a simple empty ListBuilder can be returned. + inline typename T::Reader getReader() const; inline bool operator==(decltype(nullptr)) const { return builder == nullptr; } diff --git a/c++/src/capnp/test.capnp b/c++/src/capnp/test.capnp index d2b9f819bb792510cfc611e3b1f8b2dab52523fa..b9150d74b68ef59c85440232e96b39fc323d4d0f 100644 --- a/c++/src/capnp/test.capnp +++ b/c++/src/capnp/test.capnp @@ -162,6 +162,9 @@ struct TestDefaults { struct TestObject { objectField @0 :Object; + + # Do not add any other fields here! Some tests rely on objectField being the last pointer + # in the struct. } struct TestOutOfOrder { @@ -478,6 +481,8 @@ struct TestStructUnion { } } +struct TestEmptyStruct {} + struct TestConstants { const voidConst :Void = void; const boolConst :Bool = true;