So I decided to have a play at writing a canonicalisation routine as I couldn't find anyone else's.
It should, in principle, be easy, as it's just a bunch of rules, but here's a few increasingly hard questions. I'll ignore the envelope as generating that part is trivial.
Suppose I encode this:struct Null {
}
I should presumably output a null pointer (ie. one word, equal to zero). Unambiguous? I notice the official encoder uses (-1 << 2) to represent it as a root, and (0) when it's a field.
Now suppose it's not so null:struct AnInt {
i @0 :Int64 = 0;
}
If I encode (i = 0) should I again write just a single null pointer? Any value in offset would violate a bounds check.
Let's try for something harder, now.struct FunkyStruct {
willBeNull @0 :AnInt;
willNotBeNull @1 :AnInt;
}
I encode (willBeNull = (i=0), willNotBeNull = (i=1)). Now we're getting ambiguous. willBeNull, in pre-order, must start after the end of the root message. So do I encode this as (2<<48, 1<<2, 1<<32, 1) or as (2<<48, 0, 1<<32, 1)? That is, should my empty struct become a null pointer or a pointer of length 0 to the area just after the root message? Both of these are valid according to the canonical spec, and as far as I can tell are valid encodings, too (the former being output by the official 'capnp' tool when I encode '(willNotBeNull=(i=1))').
To be clear, my complaint is that the following both appear to be canonical and equal messages:0000 0000 0000 0200
0400 0000 0000 0000
0000 0000 0100 0000
0100 0000 0000 0000
and:0000 0000 0000 0200
0000 0000 0000 0000
0000 0000 0100 0000
0100 0000 0000 0000
OK, this is just ambiguity. I would guess the intention is to encode the null pointer in this case (new rule: if struct length is zero, offset must be zero also). BUT!
Suppose I upgrade to:struct AnInt {
i @0 :Int64 = 0;
j @1 :Int64 = 1;
}
struct FunkyStruct {
willBeNull @0 :AnInt = (j=0);
willNotBeNull @1 :AnInt = (j=0);
}
Now the two encodings listed above actually have different values for willBeNull.j (the first has j=0, the second j=1)! So while it seems obvious to delete empty structs, it can fundamentally alter the message as seen by an upgraded client -- and we actually need a way to encode zero-length structs with an offset to disambiguate the two cases.
NEXT!struct AnInt {
i @0 :Int64 = 0;
}
struct DefaultInt {
i @0 :Int64 = 1;
}
struct DefaultStruct {
willBeNull @0 :DefaultInt;
willNotBeNull @1 :AnInt = (i=1);
}
Now I encode (willBeNull = (i=1), willNotBeNull = (i=1)). The official encoder produces (2<<48, 1<<2+1<<32, 1<<2+1<<32, 0, 1) as we expect. Canonicalisation says I must encode willBeNull as a zero-length struct (ie. null?).
Fine. What about willNotBeNull? Without reference to the schema, we must encode this as-is, so (2<<48, 0, 1<<32, 1) is canonical. BUT. This message is equal to (2<<48, 0, 0)
which is equal to (0). In fact, I can get the official encoder to produce the first of these two by encoding '()'. Which of these encodings are canonical?
To be clear, my complaint is that the following encodings are canonical, but equal as messages:0000 0000 0000 00000000 0000 0000 0200
0000 0000 0000 0000
0000 0000 0100 0000
0100 0000 0000 0000
Since both are valid encodes, and both are canonical according to the spec, does defined-ness of a field alter the canonical encoding of a message, even though any attempt to directly read any field from either message will yield the same answer?
This obviously affects upgraded clients in the same way as above, too.
There are parallel issues with respect to lists. We can't null an empty list (even though null is a valid encoding of it, if it's the default), and upgrading from "List(T) = [(i=0)]" to "List(T) = [(i=0, j=1)]" where j is a new field is undefined unless j=1 is default on T also.
The upgrade stuff is of particular concern; the spec doesn't mention that adding new composite fields with defaults might see the defaults violated in upgraded messages, though knowing the encoding makes it fairly obvious. I believe the canonicalisation stuff is fixable provided it is made clear that assigned-ness is preserved, and there is get a special value for "pointer to a zero length struct with offset zero" (which must be generated by implementations when they realise empty structs like Null above, also).
--
You received this message because you are subscribed to the Google Groups "Cap'n Proto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to capnproto+...@googlegroups.com.
Visit this group at https://groups.google.com/group/capnproto.
WRT when to encode as zero, I think you're arguing between "a pointer which is not null and would pre-order encode to 0 must have offset -1" and "a struct pointer which has zero length must have offset -1". The second rule seems shorter, and I'm a big fan of golf. :)
The official implementation does not implement these rules, by the way:
$ capnp --version
Cap'n Proto version 0.5.3
$ tail test.capnp -n 6
struct Null {
}
struct NuNull {
i @0 :Null;
j @1 :Null;
}
$ echo '(i=(), j=())' | capnp encode test.capnp NuNull | xxd
00000000: 0000 0000 0300 0000 0000 0000 0000 0200 ................
00000010: fcff ffff 0000 0000 0000 0000 0000 0000 ................
$ echo '(i=(), j=())' | capnp encode test.capnp NuNull | capnp decode test.capnp NuNull
(i = ())
Seems like it's implementing non-null -1 pointers exactly when they /aren't/ needed, though I didn't check the source!
The official implementation does not implement these rules, by the way:
$ capnp --version
Cap'n Proto version 0.5.3
$ tail test.capnp -n 6
struct Null {
}
struct NuNull {
i @0 :Null;
j @1 :Null;
}$ echo '(i=(), j=())' | capnp encode test.capnp NuNull | xxd
00000000: 0000 0000 0300 0000 0000 0000 0000 0200 ................
00000010: fcff ffff 0000 0000 0000 0000 0000 0000 ................
$ echo '(i=(), j=())' | capnp encode test.capnp NuNull | capnp decode test.capnp NuNull
(i = ())
Seems like it's implementing non-null -1 pointers exactly when they /aren't/ needed, though I didn't check the source!
- In the upgrade page, there is no mention of the trap where you upgrade a struct S and assign a default value D to the new field that is different to the default value D2 of the field as a sub-field of a second struct S2. I guess the easy way to say this is "any default value for a field with struct/list type T must not assign a value to a new field of T which is different to its default" or some-such. It's not at all obvious from the spec that this is the case, and I'll bet it'll catch someone out!
Maybe I'm misunderstanding, but isn't this covered by:> You cannot change a field or method parameter’s type or default value.(from the docs at https://capnproto.org/language.html#evolving-your-protocol)