Val: Introduce ZValSlot

This introduces a new class for replacing the std::optional in
RecordVal's record_val vector. It may also be useful within
VectorVal.
This commit is contained in:
Arne Welzel 2025-09-27 15:28:32 +02:00
parent 00ad3e31c6
commit 27777ac214
2 changed files with 236 additions and 0 deletions

View file

@ -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<zeek::CountVal>(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<zeek::RecordType>("conn_id_ctx");
auto v = zeek::make_intrusive<zeek::RecordVal>(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<zeek::CountVal>(42);
auto v2 = zeek::make_intrusive<zeek::CountVal>(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<zeek::RecordType>("conn_id_ctx");
auto v1 = zeek::make_intrusive<zeek::RecordVal>(t);
auto v2 = zeek::make_intrusive<zeek::RecordVal>(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<zeek::RecordType>("conn_id_ctx");
auto v1 = zeek::make_intrusive<zeek::RecordVal>(t);
auto v2 = zeek::make_intrusive<zeek::RecordVal>(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<zeek::RecordType>("conn_id_ctx");
auto v = zeek::make_intrusive<zeek::RecordVal>(t);
zeek::ZValSlot slot1 = zeek::ZValSlot(v, t);
zeek::ZValSlot slot2 = slot1;
CHECK(v->RefCnt() == 3); // v1, slot1 and slot2
}
TEST_SUITE_END();

129
src/Val.h
View file

@ -1114,6 +1114,135 @@ struct is_zeek_val {
template<typename T>
inline constexpr bool is_zeek_val_v = is_zeek_val<T>::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<ZVal>
* 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);