diff --git a/src/Val.cc b/src/Val.cc index 9c5da7e962..d07b8a3288 100644 --- a/src/Val.cc +++ b/src/Val.cc @@ -44,6 +44,8 @@ #include "zeek/broker/Store.h" #include "zeek/threading/formatters/detail/json.h" +#include "zeek/3rdparty/doctest.h" + using namespace std; namespace zeek { @@ -4120,3 +4122,108 @@ const PortValPtr& ValManager::Port(uint32_t port_num) { } } // namespace zeek + +TEST_SUITE_BEGIN("ZValSlot"); + +TEST_CASE("default constructor") { + // default constructor doesn't do anything. + zeek::ZValSlot slot; +} + +TEST_CASE("slot hold CountVal") { + auto t = zeek::base_type(zeek::TYPE_COUNT); + auto v = zeek::make_intrusive(104242); + zeek::ZValSlot slot = zeek::ZValSlot(v, t); + + CHECK(slot.IsSet()); + CHECK(slot.Tag() == zeek::TYPE_COUNT); + CHECK(! slot.IsManaged()); + CHECK(v->RefCnt() == 1); // Not managed, so the slot does not hold a ref to the original value. + CHECK(slot->AsCount() == 104242); + + auto nv = slot->ToVal(t); + CHECK(nv->RefCnt() == 1); // Not managed, so the slot does not hold a ref to the original value. +} + +TEST_CASE("slot hold RecordVal") { + auto t = zeek::id::find_type("conn_id_ctx"); + auto v = zeek::make_intrusive(t); + CHECK(v->RefCnt() == 1); + zeek::ZValSlot slot = zeek::ZValSlot(v, t); + + CHECK(slot.IsSet()); + CHECK(slot.Tag() == zeek::TYPE_RECORD); + CHECK(slot.IsManaged()); + CHECK(v->RefCnt() == 2); // Managed, slot takes a ref. + + slot.Reset(); + + CHECK(v->RefCnt() == 1); + v = nullptr; +} + +TEST_CASE("assign count ZVal to slot") { + auto t = zeek::base_type(zeek::TYPE_COUNT); + auto v1 = zeek::make_intrusive(42); + auto v2 = zeek::make_intrusive(100000); + + zeek::ZValSlot slot = zeek::ZValSlot(v1, t); + CHECK(v1->RefCnt() == 1); + + slot = zeek::ZVal(v2, t); + + // Unmanaged + CHECK(v1->RefCnt() == 1); + CHECK(v2->RefCnt() == 1); + + auto v3 = slot->ToVal(t); + + // New CountVal for 100000 + CHECK(v3 != v2); + CHECK(v3->RefCnt() == 1); +} + +TEST_CASE("assign record ZVal to slot") { + auto t = zeek::id::find_type("conn_id_ctx"); + auto v1 = zeek::make_intrusive(t); + auto v2 = zeek::make_intrusive(t); + + zeek::ZValSlot slot = zeek::ZValSlot(v1, t); + CHECK(v1->RefCnt() == 2); // v1 and slot + + slot = zeek::ZVal(v2, t); + CHECK(v1->RefCnt() == 1); // slot released + CHECK(v2->RefCnt() == 2); // v2 and slot + + auto v3 = slot->ToVal(t); + CHECK(v3 == v2); + CHECK(v2->RefCnt() == 3); // v2, slot and v3 +} + +TEST_CASE("assign slot assignment") { + auto t = zeek::id::find_type("conn_id_ctx"); + auto v1 = zeek::make_intrusive(t); + auto v2 = zeek::make_intrusive(t); + + zeek::ZValSlot slot1 = zeek::ZValSlot(v1, t); + zeek::ZValSlot slot2 = zeek::ZValSlot(v2, t); + + CHECK(v1->RefCnt() == 2); // v1 and slot1 + CHECK(v2->RefCnt() == 2); // v2 and slot2 + + slot1 = slot2; + CHECK(v1->RefCnt() == 1); // slot1 released + CHECK(v2->RefCnt() == 3); // v2, slot1 and slot2 +} + +TEST_CASE("copy slot") { + auto t = zeek::id::find_type("conn_id_ctx"); + auto v = zeek::make_intrusive(t); + + zeek::ZValSlot slot1 = zeek::ZValSlot(v, t); + zeek::ZValSlot slot2 = slot1; + + CHECK(v->RefCnt() == 3); // v1, slot1 and slot2 +} + +TEST_SUITE_END(); diff --git a/src/Val.h b/src/Val.h index 17cc735d0e..517ad51a3b 100644 --- a/src/Val.h +++ b/src/Val.h @@ -1114,6 +1114,135 @@ struct is_zeek_val { template inline constexpr bool is_zeek_val_v = is_zeek_val::value; +/** + * A ZValSlot holds a ZVal instance and some auxiliary information that allows automatic + * memory management as well as acting as an optional unset field. + * + * This class originated from the observation that a std::optional + * as previously used in VectorVal and RecordVal objects already takes up + * 16 bytes on 64 bit architectures with GCC. The ZValSlot class essentially + * uses the left-over 7 bytes from the std::optional to allow easier memory + * management without needing to keep external auxilarly information around. + * + * The is_managed flag and type_tag flags are meant to be immutable except + * when a ZValSlot is reassigned. + * + * A ZValSlot instance holds a reference to a managed value. Such ZValSlot + * instances can be copied or assigned and the reference count of the ZVal + * will be updated accordingly. + */ +class ZValSlot { +public: + /** + * Totally uninitialized, watch out! + */ + ZValSlot() {} + + /** + * Initialize a set ZValSlot given a ValPtr and corresponding TypePtr. + * + * This has the same ref counting semantics as the corresponding ZVal + * constructor, increasing the ref count of any managed value. + */ + ZValSlot(ValPtr v, const TypePtr& t) + : is_set(true), is_managed(ZVal::IsManagedType(t)), type_tag(t->Tag()), zval(v, t) {} + + /** + * Initialize a ZValSlot with just the TypePtr. + * + * This is useful for optional fields in a record value where + * the type is known at construction time. + */ + ZValSlot(const TypePtr& t) : is_set(false), is_managed(ZVal::IsManagedType(t)), type_tag(t->Tag()) {} + + /** + * Copy constructor. + */ + ZValSlot(const ZValSlot& s) : is_set(s.is_set), is_managed(s.is_managed), type_tag(s.type_tag), zval(s.zval) { + if ( is_set && is_managed ) + Ref(zval.ManagedVal()); + } + + /** + * Destructor. + */ + ~ZValSlot() { Reset(); } + + ZValSlot& operator=(const ZValSlot& s) { + if ( is_set && is_managed ) + Unref(zval.ManagedVal()); + + is_set = s.is_set; + is_managed = s.is_managed; + type_tag = s.type_tag; + zval = s.zval; + + if ( is_set && is_managed ) + Ref(zval.ManagedVal()); + + return *this; + } + + /** + * Assign a ZVal to a slot. + * + * This uses the is_managed member to determine if the + * given ZVal should be treated as managed and whether. + * + * Assigning a ZVal to a managed slot adopts a reference! + * This is a bit quirky, but it's what the plain ZVal + * constructors also do. + */ + ZValSlot& operator=(const ZVal& zv) { + if ( is_set && is_managed ) + Unref(zval.ManagedVal()); + + is_set = true; + zval = zv; + + return *this; + } + + operator bool() const noexcept { return is_set; } + const ZVal* operator->() const noexcept { return &zval; } + ZVal& operator*() noexcept { return zval; } + const ZVal& operator*() const noexcept { return zval; } + + bool IsSet() const noexcept { return is_set; } + bool IsManaged() const noexcept { return is_managed; } + TypeTag Tag() const noexcept { return type_tag; } + + void Reset() { + if ( is_set && is_managed ) + Unref(zval.ManagedVal()); + + is_set = false; + } + + /** + * Convert a slot's ZVal to a ValPtr given a TypePtr. + * + * @param t Type to use for conversion to Val. Needs to agree with the type that was used to initialize the + * slot. + * + * @return A ValPtr instance for the slot. + */ + ValPtr ToVal(const TypePtr& t) { + assert(IsSet()); + // assert(Tag() == TYPE_ANY || Tag() == t->Tag()); + + return zval.ToVal(t); + } + +private: + bool is_set; + bool is_managed; + TypeTag type_tag; + ZVal zval; +}; + +static_assert(sizeof(ZValSlot) <= 16); + class RecordVal final : public Val, public notifier::detail::Modifiable { public: explicit RecordVal(RecordTypePtr t, bool init_fields = true);