blob: a0b0b7a32284b069f40bd07d2c999f5723cae39d [file] [log] [blame] [view]
# Emboss User Guide
## Getting Started
First, you must identify a data structure you want to read and write. These are
often documented in hardware manuals a bit like [this one, for the fictional
BN-P-6000404 illuminated button panel](BogoNEL_BN-P-6000404_User_Guide.pdf). We
will use the BN-P-6000404 as an example.
### A Caution
Emboss is still beta software. While we believe that we will not need to make
any more breaking changes before 1.0, you may still encounter bugs and there are
many missing features.
You can contact `emboss-dev@google.com` with any issues. Emboss is not an
officially supported Google product, but the Emboss authors will try to answer
emails.
### System Requirements
#### Running the Emboss Compiler
The Emboss compiler requires Python 3.6 or later. On a Linux-like system with
Python 3 installed in the usual place (`/usr/bin/python3`), you can run the
embossc script at the top level on an `.emb` file to generate C++, like so:
```
embossc --generate cc --output-path path/to/object/dir path/to/input.emb
```
If your project is using Bazel, the `build_defs.bzl` file has an
`emboss_cc_library` rule that you can use from your project.
#### Using the Generated C++ Code
The code generated by Emboss requires a C++11-compliant compiler, and a
reasonably up-to-date standard library. Emboss has been tested with GCC and
Clang, libc++ and libstd++. In theory, it should work with MSVC, ICC, etc., but
it has not been tested, so there are likely to be bugs.
The generated C++ code lives entirely in a `.h` file, one per `.emb` file. All
of the generated code is in C++ templates or (in a very few cases) `inline`
functions. The generated code is structured this way in order to implement
"pay-as-you-use" for code size: any functions, methods, or views that are not
used by your code won't end up in your final binary. This is often important
for environments like microcontrollers!
There is an Emboss runtime library (under `runtime/cpp`), which is also
header-only. You will need to add the root of the Emboss source tree to your
`#include` path.
Note that it is *strongly* recommended that you compile your release code with
at least some optimizations: `-Os` or `-O2`. The Emboss generated code leans
fairly heavily on your C++ compiler's inlining and common code elimination to
produce fast, lean compiled code.
#### Contributing to the Compiler
If you want to contribute features or bugfixes to the Emboss compiler itself,
you will need Bazel to run the Emboss test suite.
### Create an `.emb` file
Next, you will need to translate your structures.
```
[$default byte_order: "LittleEndian"]
[(cpp) namespace: "bogonel::bnp6000404"]
```
The BN-P-6000404 uses little-endian numbers, so we can set the default byte
order to `LittleEndian`. There is no particular C++ namespace implied by the
BN-P-6000404 user guide, so we use one that is specific to the BN-P-6000404.
The BN-P-6000404, like many devices with serial interfaces, uses a framed
message system, with a fixed header and a variable message body depending on a
message ID. For the BN-P-6000404, this framing looks like this:
<!-- TODO(bolms): finalize the "magic value initialization" feature, document it
here. -->
```
struct Message:
-- Top-level message structure, specified in section 5.3 of the BN-P-6000404
-- user guide.
0 [+1] UInt sync_1
[requires: this == 0x42]
1 [+1] UInt sync_2
[requires: this == 0x4E]
2 [+1] MessageId message_id
-- Type of message
3 [+1] UInt message_length (ml)
-- Length of message, including header and checksum
# ... body fields to follow ...
```
We could have chosen to put the header fields into a separate `Header` structure
instead of placing them directly in the `Message` structure.
The `sync_1` and `sync_2` fields are required to have specific magic values, so
we add the appropriate `[requires: ...]` attributes to them. This tells Emboss
that if those fields do not have those values, then the `Message` `struct` is
ill-formed: in the client code, the `Message` will not be `Ok()` if those fields
have the wrong values, and Emboss will not allow wrong values to be written into
those fields using the checked (default) APIs.
Unfortunately, BogoNEL does not provide a nice table of message IDs, but
fortunately there are only a few, so we can gather them from the individual
messages:
```
enum MessageId:
-- Message type idenfiers for the BN-P-6000404.
IDENTIFICATION = 0x01
INTERACTION = 0x02
QUERY_IDENTIFICATION = 0x10
QUERY_BUTTONS = 0x11
SET_ILLUMINATION = 0x12
```
Next, we should translate the individual messages to Emboss.
```
struct Identification:
-- IDENTIFICATION message, specified in section 5.3.3.
0 [+4] UInt vendor
# 0x4F474F42 is "BOGO" in ASCII, interpreted as a 4-byte little-endian
# value.
[requires: this == 0x4F47_4F42]
0 [+4] UInt:8[4] vendor_ascii
-- "BOGO" for BogoNEL Corp
# The `vendor` field really contains the four ASCII characters "BOGO", so we
# could use a byte array instead of a single UInt. Since it is valid to
# have overlapping fields, we can have both `vendor` and `vendor_ascii` in
# our Emboss specification.
4 [+2] UInt firmware_major
-- Firmware major version
6 [+2] UInt firmware_minor
-- Firmware minor version
```
<!-- TODO(bolms): fixed-length, ASCIIZ, and variable-length string support? -->
The `Identification` structure is fairly straightforward. In this case, we
provide an alternate view of the `vendor` field via `vendor_ascii`: 0x4F474F42
in little-endian works out to the ASCII characters "BOGO".
Note that `vendor_ascii` uses `UInt:8[4]` for its type, and not `UInt[4]`. For
most fields, we can use plain `UInt` and Emboss will figure out how big the
`UInt` should be, but for an array we must be explicit that we want 8-bit
elements.
```
struct Interaction:
-- INTERACTION message, specified in section 5.3.4.
0 [+1] UInt number_of_buttons (n)
-- Number of buttons currently depressed by user
4 [+n] ButtonId:8[n] button_id
-- ID of pressed button. A number of entries equal to number_of_buttons
-- will be provided.
```
<!-- TODO(bolms): reserved field support -->
`Interaction` is also fairly straightforward. The only tricky bit is the
`button_id` field: since `Interaction` can return a variable number of button
IDs, depending on how many buttons are currently pressed, the `button_id` field
must has length `n`. It would have been OK to use `[+number_of_buttons]`, but
full field names can get cumbersome, particularly when the length involves are
more complex expression. Instead, we set an *alias* for `number_of_buttons`
using `(n)`, and then use the alias in `button_id`'s length. The `n` alias is
not visible outside of the `Interaction` message, and won't be available in the
generated code, so the short name is not likely to cause confusion.
```
enum ButtonId:
-- Button IDs, specified in table 5-6.
BUTTON_A = 0x00
BUTTON_B = 0x04
BUTTON_C = 0x08
BUTTON_D = 0x0C
BUTTON_E = 0x01
BUTTON_F = 0x05
BUTTON_G = 0x09
BUTTON_H = 0x0D
BUTTON_I = 0x02
BUTTON_J = 0x06
BUTTON_K = 0x0A
BUTTON_L = 0x0E
BUTTON_M = 0x03
BUTTON_N = 0x07
BUTTON_O = 0x0B
BUTTON_P = 0x0F
```
We had to prefix all of the button names with `BUTTON_` because Emboss does not
allow single-character enum names.
The QUERY IDENTIFICATION and QUERY BUTTONS messages don't have any fields other
than `checksum`, so we will handle them a bit differently.
```
struct SetIllumination:
-- SET ILLUMINATION message, specified in section 5.3.7.
0 [+1] bits:
0 [+1] Flag red_channel_enable
-- Enables setting the RED channel.
1 [+1] Flag blue_channel_enable
-- Enables setting the BLUE channel.
2 [+1] Flag green_channel_enable
-- Enables setting the GREEN channel.
1 [+1] UInt blink_duty
-- Sets the proportion of time between time on and time off for blink
-- feature.
--
-- Minimum value = 0 (no illumination)
--
-- Maximum value = 240 (constant illumination)
[requires: 0 <= this <= 240]
2 [+2] UInt blink_period
-- Sets the blink period, in milliseconds.
--
-- Minimum value = 10
--
-- Maximum value = 10000
[requires: 10 <= this <= 10_000]
4 [+4] bits:
0 [+32] UInt:2[16] intensity
-- Intensity values for the unmasked channels. 2 bits of intensity for
-- each button.
```
`SetIllumination` requires us to use bitfields. The first bitfield is in the
CHANNEL MASK field: rather than making a single `channel_mask` field, Emboss
lets us specify the red, green, and blue channel masks separately.
As with `sync_1` and `sync_2`, we have added `[requires: ...]` to the
`blink_duty` and `blink_period` fields: this time, specifying a range of valid
values. `[requires: ...]` accepts an arbitrary expression, which can be as
simple or as complex as desired.
It is not clear from BogoNEL's documentation whether "bit 0" means the least
significant or most significant bit of its byte, but a little experimentation
with the device shows that setting the least significant bit causes
`SetIllumination` to set its red channel. Emboss always numbers bits in
bitfields from least significant (bit 0) to most significant.
The other bitfield is the `intensity` array. The BN-P-6000404 uses an array of
2 bit intensity values, so we specify that array.
Finally, we should add all of the sub-messages into `Message`, and also take
care of `checksum`. After making those changes, `Message` looks like:
```
struct Message:
-- Top-level message structure, specified in section 5.3 of the BN-P-6000404
-- user guide.
0 [+1] UInt sync_1
[requires: this == 0x42]
1 [+1] UInt sync_2
[requires: this == 0x4E]
2 [+1] MessageId message_id
-- Type of message
3 [+1] UInt message_length (ml)
-- Length of message, including header and checksum
if message_id == MessageId.IDENTIFICATION:
4 [+ml-8] Identification identification
if message_id == MessageId.INTERACTION:
4 [+ml-8] Interaction interaction
if message_id == MessageId.SET_ILLUMINATION:
4 [+ml-8] SetIllumination set_illumination
0 [+ml-4] UInt:8[] checksummed_bytes
ml-4 [+4] UInt checksum
```
By wrapping the various message types in `if message_id == ...` constructs,
those substructures will only be available when the `message_id` field is set to
the corresponding message type. This kind of selection is used for any
structure field that is only valid some of the time.
The substructures all have the length `ml-8`. The `ml` is a short alias for the
`message_length` field; these short aliases are available so that the field
types and names don't have to be pushed far to the right. Aliases may only be
used directly in the same structure definition where they are created; they may
not be used elsewhere in an Emboss file, and they are not available in the
generated code. The length is `ml-8` in this case because the `message_length`
includes the header and checksum, which left out of the substructures.
Note that we simply don't have any subfield for QUERY IDENTIFICATION or QUERY
BUTTONS: since those messages do not have any fields, there is no need for a
zero-byte structure.
We also added the `checksummed_bytes` field as a convenience for computing the
checksum.
### Generate code
Once you have an `.emb`, you will need to generate code from it.
The simplest way to do so is to run the `embossc` tool:
```
embossc -I src --generate cc --output-path generated bogonel.emb
```
The `-I` option adds a directory to the *include path*. The input file -- in
this case, `bogonel.emb` -- must be found somewhere on the include path.
The `--generate` option specifies which back end to use; `cc` is the C++ back
end.
The `--output-path` option specifies where the generated file should be placed.
Note that the output path will include all of the path components of the input
file: if the input file is `x/y/z.emb`, then the path `x/y/z.emb.h` will be
appended to the `--output-path`. Missing directories will be created.
<!-- #### Using Bazel -->
<!-- TODO(bolms): Make this usable from Bazel. -->
### Include the generated C++ code
Emboss generates a single C++ header file from your `.emb` by appending `.h` to
the file name: to use the BogoNEL definitions, you would `#include
"path/to/bogonel.emb.h"` in your C++ code.
Currently, Emboss does not generate a corresponding `.cc` file: the code that
Emboss generates is all templates, which exist in the `.h`. Although the Emboss
maintainers (e.g., bolms@) like the simplicity of generating a single file, this
could change at some point.
### Use the generated C++ code
Emboss generates *views*, which your program can use to read and write existing
arrays of bytes, and which do not take ownership. For example:
```c++
#include "path/to/bogonel.emb.h"
template <typename View>
bool ChecksumIsCorrect(View message_view);
// Handles BogoNEL BN-P-6000404 device messages from a byte stream. Returns
// the number of bytes that were processed. Unprocessed bytes should be
// passed into the next call.
int HandleBogonelPanelMessages(const char *bytes, int byte_count) {
auto message_view = bogonel::bnp6000404::MakeMessageView(bytes, byte_count);
// IsComplete() will return true if the view has enough bytes to fully
// contain the message; i.e., that byte_count is at least
// message_view.message_length().Read() + 4.
if (!message_view->IsComplete()) {
return 0;
}
// If Emboss is happy with the message, we still need to check the checksum:
// Emboss does not (yet) have support for automatically checking checksums and
// CRCs.
if (!message_view->Ok() || !ChecksumIsCorrect(message_view)) {
// If the message is complete, but not correct, we need to log an error.
HandleBrokenMessage(message_view);
return message_view->Size();
}
// At this point, we know the message is complete and (basically) OK, so
// we dispatch it to a message-type-specific handler.
switch (message_view->message_id().Read()) {
case bogonel::bnp6000404::MessageId::IDENTIFICATION:
HandleIdentificationMessage(message_view);
break;
case bogonel::bnp6000404::MessageId::INTERACTION:
HandleInteractionMessage(message_view);
break;
case bogonel::bnp6000404::MessageId::QUERY_IDENTIFICATION:
case bogonel::bnp6000404::MessageId::QUERY_BUTTONS:
case bogonel::bnp6000404::MessageId::SET_ILLUMINATION:
Log("Unexpected host to device message type.");
break;
default:
Log("Unknown message type.");
break;
}
return message_view->Size();
}
template <typename View>
bool ChecksumIsCorrect(View message_view) {
uint32_t checksum = 0;
for (int i = 0; i < message_view.checksum_bytes().ElementCount(); ++i) {
checksum += message_view.checksum_bytes()[i].Read();
}
return checksum == message_view.checksum().Read();
}
```
<!-- TODO(bolms): solidify support for checksums, so that the Ok() call in the
example actually checks them. -->
The `message_view` object in this example is a lightweight object that simply
provides *access* to the bytes in `message`. Emboss views are very cheap to
construct because they only contain a couple of pointers and a length -- they do
not copy or take ownership of the underlying bytes. This also means that you
have to keep the underlying bytes alive as long as you are using a view -- you
can't let them go out of scope or delete them.
Views can also be used for writing, if they are given pointers to mutable
memory:
```c++
void ConstructSetIlluminationMessage(const vector<bool> &lit_buttons,
vector<char> *result) {
// The SetIllumination message has a constant size, so SizeInBytes() is
// available as a static method.
int length = bogonel::bnp6000404::SetIllumination::SizeInBytes() + 8;
result->clear();
result->resize(length);
auto view = bogonel::bnp6000404::MakeMessageView(result);
view->sync_1().Write(0x42);
view->sync_2().Write(0x4E);
view->message_id().Write(bogonel::bnp6000404::MessageId::SET_ILLUMINATION);
view->message_length().Write(length);
view->set_illumination().red_channel_enable().Write(true);
view->set_illumination().blue_channel_enable().Write(true);
view->set_illumination().green_channel_enable().Write(true);
view->set_illumination().blink_duty().Write(240);
view->set_illumination().blink_period().Write(10000);
for (int i = 0; i < view->set_illumination().intensity().ElementCount();
++i) {
view->set_illumination().intensity()[i].Write(lit_buttons[i] ? 3 : 0);
}
}
```
### Use the `.emb` Autoformatter
You can use the `.emb` autoformatter to avoid manual formatting. For now, it is
available at `compiler/front_end/format.py`.
*TODO(bolms): Package the Emboss tools for easy workstation installation.*