Saturday, April 6, 2019

C++ Cleverness

Yukari was in some ways an amazing piece of code. It didn't actually drive the robot all that well, but of all the things in it, I am most proud of it's self-documentation. Each run of Yukari produced a file which recorded three things:


  1. Data packets showing what the robot was doing and what it was thinking
  2. An image of its code and any other files I thought needed attaching
  3. A description of how to parse the record file, partly in English, partly machine readable. With this description, anyone who had the file could in principle write a piece of code to parse the file.
I am redoing this code for the Loginator. While it is easy to just use the same logic that I used on Yukari, that code was inefficient. It did the following:

  • The packet start function took a pointer to a string describing the packet, and each kind of fill function took a pointer to a string describing the field. This was nice because the documentation for each field in the code is right next to the actual code for it.
  • The start function and each fill function called the writeDoc() function, which took care of documentaion. After that was finished, it wrote the field.
  • The writeDoc() function kept track of apids which have already been documented. If this apid has already been documented, writeDoc() returns immediately. Otherwise it write a field description packet.
  • In order to make this work, the actual packet data had to be stashed somewhere. If the packet was in the process of being documented, writeDoc() for a packet start set up pointers such that the packet being written went to this stash buffer, and writeDoc() got to create actual packets in the proper buffer.
What I have in mind is much more clever. It will see the compiler generate the core of the documentation packets at compile-time. These will then just be in ROM, which we have bucketloads of. I think this will involve template meta-programming and constexpr functions. 

Each call to start and fill will be immediately followed by a template class instantiation. This class template will take as parameters the apid, the string description, and perhaps some other stuff (units, conversion, etc). The class will declare a couple of static constant member fields, which will result in them getting stuck in the .rodata section, or maybe a special section. Once it is someplace in the read-only image, the startup code will write out all the documentation in the same way that 

The interface will look like this:

start(apid_blah,TTC(0)); template class packet_doc<apid_blah,0,"This packet records exactly how blah things are">;
fillu16(blah); template class packet_doc<apid_blah,t_u16,"This field records the blah level">;

It's not quite as clean as the old way, but it is purely a run-time thing. To begin with, we would have a template something like this:

template<int A, int T, const char* D>
class packet_doc {
  static const int __attribute__ ((section(".packet_doc"))) apid=A;
  static const int __attribute__ ((section(".packet_doc"))) fieldType=T;

  static const char* __attribute__ ((section(".packet_doc"))) desc=D;
}

We could make things fancier by keeping track of the position in the packet using more advanced template metaprogramming.

Update:
Nope, defeated. While you can use an address as a template parameter, a string literal doesn't necessarily have an address on its own. You can set up a const array with a string literal in it, and use that as the template parameter, but that starts getting way too ugly. I'll do it the old way, and use the bottom of the stack for temporary space. It isn't secure, because what happens if the stack and this buffer collide, but I'm not going to worry about that.