Hi Brano,
Post by Branislav KatreniakGetters and setters will make the code the api more consistent with Reader
and Builder. The prefixes `set` and `get` are great in generated code,
because they move all fields into kind of namespace. No collisions with
language keywords, no collisions with generic methods like `clone()`.
That's a good point. Though, we already work around language keyword
collisions by appending a trailing underscore to conflicting names.
Post by Branislav KatreniakWhat about API for groups? Can they be pure references into parent POCS
class without owned memory? If not, I have hard time to imagine how to
effectively reuse serialized format for group POCS class. For me, this
looks like a show stopper for serialized format in POCS classes.
I think it would be reasonable for group accessors to return a reference.
For that matter, sub-message accessors would probably return a reference
too, except that presumably there'd be a method you can call to have the
sub-message disowned which would then return Own<T>. The disown method
would not be available for groups.
Post by Branislav KatreniakAs I am trying to implement generator for POCS classes, I have few
questions.
It's cool that you're working on this. Note that I tend to have strong
opinions on APIs, so it might be a good idea to write up a doc or something
with a proposed API before going too far into implementation. :)
Post by Branislav KatreniakWhat is good name for POCS class in source code? The generated code is
placed in class that was called "outer" class now. It would be good to come
with terminology that can be used also in documentation.
I think the outer class itself (which currently behaves only as a
namespace) should be the POCS type. This makes the POCS interface very
natural to use, which is its goal in life after all.
Post by Branislav KatreniakWhat is api to primitive fields? It is not possible to return reference to
primitive type, because it can have different byte order and it may be
xored with default value. It needs either setter and getter or proxy type.
I start simple with setter and getter. Proxy type can be introduced later.
uint32_t getNumber() const;
void setNumber(uint32_t);
I think it's important to settle on one API now -- I don't want to end up
with multiple ways of doing things.
I think the "plain old fields" approach is a nicer API than accessors if we
can make it work, so the question is: is there any reason we can't make it
work? I think we have to go through all of the features and think about
whether there is an issue.
I just realized: We don't need proxies. We can use regular integer types,
and we say that endianness is translated during the copy between wire
format and POCS. Since almost all CPUs are little-endian, on almost all
CPUs we'll still be able to use a memcpy() -- only on big-endian CPUs will
the translation have to fall back to handling each field. I think this is a
far better plan than using proxy types in POCS since proxies have so many
problems.
So let's list out some things:
Primitives:
* Void: C++ does not let you declare zero-width fields, but since voids
don't affect the ultimate encoding we could pull the void fields out to the
beginning or end of the structure.
* Boolean: Use bitfields.
* Integers/Floats: Use regular types.
* Enums: Use C++11 enum classes with uint16_t as the base type. In fact the
enum types we're already generating should work for this.
Pointers: No need for perfect alignment here since we obviously can't
memcpy() them anyway.
* Text: kj::String
* Data: kj::Array<kj::byte>
* List(T): kj::Vector<T>. This implies bool lists will expand to
byte-per-element in POCS format, not bit-per-element as they are on the
wire. That's probably OK; these aren't used very often. (Of course,
std::vector<bool> would keep them as bits but everyone seems to agree that
was a disaster.)
* Structs: kj::Own<T>
* Capabilities: T::Client (I wonder if we should move T::Client's members
into T and make T::Client be an alias for backwards-compatibility?)
Null pointers: I suppose that String, Array, Vector, and Own will all need
to support comparison with nullptr. String and Array do already, but they
are equivalent to comparing with the empty string / array. This is arguably
incorrect, although in practice I'd say programs should avoid
distinguishing between null and empty. If we decide this needs to be
distinguishable, then we probably need to introduce new types here.
Probably, we'd use capnp::Text and capnp::List<T>, which is arguably more
consistent with the rest of the API anyhow.
Groups: Described previously. The whole struct would be an anonymous union
containing an anonymous struct of the top-level fields as well as named
structs for each group, carefully padded to align with each other.
Unions: This is tricky. Possibilities:
- Use a proxy type. Since a union member could itself be a struct, and
there's no way to proxy arbitrary members, we'd probably need to use
operator->(): foo.unionField->member. But then it's hard for us to tell
whether the union member is being accessed for read or for write, which is
important because on read we want to throw an exception if it's not the
active member and on write we want to make it active. We'd probably have to
assume the latter (unless the struct is const).
- Use an accessor method returning a reference. This is similar to the
previous point but instead of foo.unionField->member you'd have
foo.unionField().member. Unclear whether this is more or less confusing.
- Revert all the way to getFoo(), setFoo(), initFoo(). Sadness.
Another issue with Unions is how to handle "which". It seems like this
should be a method too, since we don't want people directly overwriting the
union discriminant.
Structs containing unions will need to have non-default constructors,
destructors, copy, and assignment to deal with any unioned pointers.
AnyPointer: We can have capnp::AnyPointer be a class with some arbitrary
interface for this. It would probably need to store an encoded Cap'n Proto
blob behind the scenes, which it could decode on-demand.
AnyList/AnyStruct: kj::Own<AnyList>/kj::Own<AnyStruct>, otherwise similar
to AnyPointer.
Generics: I think this is straightforward. We'll need a template typedef
Pointer<T> which expands to:
* Pointer<List<T>> -> kj::Vector<T>
* Pointer<T> | T is a capability -> T::Client
* Pointer<T> | T is a struct -> kj::Own<T>
* Pointer<AnyPointer> -> AnyPointer
* Pointer<Text> -> kj::String
* Pointer<Data> -> kj::Array<kj::byte>
So the most difficult parts are groups (seem to work, but weird) and unions
(API is inconsistent). There is also an open question about whether to use
kj::String, kj::Array, and kj::Vector vs. defining new types capnp::Text,
capnp::Data, and capnp::List<T>. I'm actually starting to lean towards the
latter for consistency's sake. We could design them to mostly reuse the KJ
types under the hood.
I think overall this strategy seems doable.
Post by Branislav KatreniakWhat is api to string field? Minimal approach that need is intrusive into
string class
kj::String& getName();
const kj::String& getName() const { return _name; }
kj::String needs to be extend with null field.
FWIW if we use accessors, I'd want to have the same set of accessors that
builders have today, so get, set, etc:
bool hasName() const;
kj::StringPtr getName() const;
void setName(kj::StringPtr);
void setName(kj::String&&);
kj::String disownName();
I would not want methods that return references, since returning a
reference defeats a lot of the purpose of accessors -- it allows someone
external to subsequently change the value of the struct (through the
reference) without any chance for the class to detect this change.
Post by Branislav KatreniakSecond approach
bool hasName() const;
kj::String& getName();
const kj::String& getName() const;
Non-const getter sets name to non-null. Const getter can return reference
to global instance if hasName() == false. This second approach is
non-intrusive and works also with std::string.
Can we afford to not support NULL flag for strings in POCS class and
encode empty string as null string on wire? That simplifies the API and
generated code.
Do we need an option to set NULL flag?
It is easy to add also convenience setter for consistency with builders
and primitive types.
void setName(kj::String &&);
I believe structs and lists can stick to the same API as strings.
Groups can be just a view into POCS class without value semantics.
Internally it will be pointer / reference to owning POCS instance. If POCS
instance is deleted or if group sits in union section that is invalidated,
group simply points to invalid memory. We could track group instances from
POCS class and clear the pointers to trade speed / nice exceptions instead
of crash. But first version can be simply without unions and groups.
Any suggestions?
Kind regards
Brano
--
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
Visit this group at https://groups.google.com/group/capnproto.
As I am trying to implement generator for POCS classes, I have few
questions.
What is good name for POCS class in source code? The generated code is
placed in class that was called "outer" class now. It would be good to come
with terminology that can be used also in documentation.
What is api to primitive fields? It is not possible to return reference to
primitive type, because it can have different byte order and it may be
xored with default value. It needs either setter and getter or proxy type.
I start simple with setter and getter. Proxy type can be introduced later.
uint32_t getNumber() const;
void setNumber(uint32_t);
What is api to string field? Minimal approach that need is intrusive into
string class
kj::String& getName();
const kj::String& getName() const { return _name; }
kj::String needs to be extend with null field.
Second approach
bool hasName() const;
kj::String& getName();
const kj::String& getName() const;
Non-const getter sets name to non-null. Const getter can return reference
to global instance if hasName() == false. This second approach is
non-intrusive and works also with std::string.
Can we afford to not support NULL flag for strings in POCS class and
encode empty string as null string on wire? That simplifies the API and
generated code.
Do we need an option to set NULL flag?
It is easy to add also convenience setter for consistency with builders
and primitive types.
void setName(kj::String &&);
I believe structs and lists can stick to the same API as strings.
Groups can be just a view into POCS class without value semantics.
Internally it will be pointer / reference to owning POCS instance. If POCS
instance is deleted or if group sits in union section that is invalidated,
group simply points to invalid memory. We could track group instances from
POCS class and clear the pointers to trade speed / nice exceptions instead
of crash. But first version can be simply without unions and groups.
Any suggestions?
Kind regards
Brano
--
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
Visit this group at https://groups.google.com/group/capnproto.
--
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.