$darkmode
Elektra 0.11.0
|
Metakeys (i.e., keys with namespace KEY_NS_META
) have special requirements beyond what other namespaces want. This is because metakeys are used for specifications.
These extra requirements are:
spec:/
keys to other namespaces must share as much memory as possible. Without sharing memory there would be massive unnecessary duplication of data.spec:/
key, or was set directly. Additionally, it must be possible to detect if the value of a copied metakey has changed.spec:/
key (and its metadata).Unrelated to the specification use case (see also Use Case: Validating Configuration with SpecificationValidating Configuration with Specification""), metakeys must also be prevented from having metadata of their own. As keyMeta
now "leaks" (compared with the previous API) the KeySet
of metadata is unprotected and the requirements above are not fulfilled anymore. For example, changes of metadata values could confuse the spec plugin and lead to invalid configuration passed to applications.
spec
could make use of specialized APIs. However, the need for such internal APIs should be limited as much as possible.keyNew()
and keyMeta()
. keyMeta()
can just return NULL
and keyNew()
can either ignore metadata arguments or fail if metadata is given.One option to enforce all requirements is to just make any Key
that is created with the KEY_NS_META
namespace entirely read-only after keyNew()
.
Because the Key
is entirely read-only, it's value cannot change. Therefore, we can find out, if it was copied from a given spec:/
key by doing a pointer comparison:
To update metadata you would have to create a new Key
to replace the existing one:
Safely copying metadata would be very simple with this solution.
This copies metadata while sharing the memory for the individual Key
s. Because the Key
s are read-only they cannot be changed at all, so we don't have to worry about changes to dest
affecting source
. If we want to change the metadata of dest
we have to create a new Key
, which will not be used by source
.
A relaxation of the above solution would be to only make the Key
read-only, while it is part of a KeySet
.
The snippets from above would still work and changing the value of metakey directly as above would still fail. But now you don't have to create the Key
directly with the right name and value.
The snippet above would not work, if the key is returned as read-only directly from keyNew()
.
Copying metadata works the same as above. Since the Key
s are still read-only as long as source
uses them, we again cannot affect source
by changing dest
.
An issue with the solutions above is that you always have to create a new Key
to change metadata. This means we need to allocate all the memory for a new metakey. This new metakey will replace the existing one in keyMeta(key)
. But that means, if the existing metakey was not shared with other keys, it will be deleted, when we could have just reused that memory.
Some of that problem is mitigated by keyDup
using copy-on-write (COW). However, let's look at what is needed to change the value of meta:/type
with a read-only solution.
The straightforward option doesn't work, because of the read-only nature of metakeys:
To make proper use of COW you need to write something like this to change the value of meta:/type
:
However, most people would probably opt for the much simpler:
For a one-time operation the difference might not be big, but if a metakey (probably not meta:/type
), changes many times it will become significant. Every time the simpler solution is used, an entirely new Key
is created instead of utilizing the COW approach.
One option to solve that read-only problems, in "Read-only while in `KeySet`" is something like this:
Note: Where the code above would live doesn't matter. It may be part of some Elektra library, or it may be user code. The code is clearly not ideal and that's why the solution here actually is to avoid the need for a
isInKeySet
function entirely.
However, that still has some obvious issues:
isInKeySet
to check whether a metakey is used by some metadata KeySet
.KeySet
and reinsert it. That means shuffling around the array in the KeySet
, which may be worse than the duplicate memory for the new metakey.Clearly this is not a viable solution to the problem. But the good thing is we can solve these issues, because of the COW implementation.
To solve the issues mentioned above, spec
needs to do a few things differently:
Instead of copying metadata with
we need to make a copy of all metakeys
This seems like a bad change, but because of the COW implementation it's not as bad as it looks. Only the struct Key
will be duplicated, both the name and value data of the metakeys will still be shared between source
and dest
. Still worse than reusing everything and just adding a few new pointers, but not too bad.
This change means that modifying metadata in dest
cannot possibly affect source
, because they do not share any Key *
s. At first, they do share all the name and value data, but through COW that stops as soon as either Key
is modified. Therefore, we don't need to make the value read-only and anybody (including user code) can just do:
The issue with not sharing Key *
s of course is that a pointer comparison will no longer detect copied metakeys. That means we need a new way of detecting what spec
needs to remove.
First we'd add a new keyGetCOWValue
function:
Note: This function wouldn't be part of
libelektra-core
. It would be inlibelektra-extra
or some other library on whichspec
would depend.
With this function, the check to detect copied metakeys becomes this straightforward snippet:
The check above works, because changing the value of a Key
will allocate a new key->keyData
, if it is shared with another key. Therefore, the keyData
pointers will only be the same if the value was not modified and was copied from the spec:/
key.
Note: Adding
keyGetCOWValue
(anywhere) might cause problem with some of the current guarantees related to COW. A quick solution for this would be to instead only providekeyHasSameCOWValue
:bool keyHasSameCOWValue (Key * key, Key * other){if (key == NULL && other == NULL) return true;if (key == NULL || other == NULL) return false;return key->keyData == other->keyData;}This would still allow making the comparison needed by
spec
, but doesn't actually provide any access tokey->keyData
, so nothing no guarantees can be broken.