-
Notifications
You must be signed in to change notification settings - Fork 1
Hammer Fundamentals
These are the core building blocks of the Hammer library. Everything you build with Hammer uses these primitives, regardless of the protocol.
A parser combinator library lets you build parsers by combining small, reusable parsers into larger ones. Instead of writing procedural code that manually reads bytes and checks values, you declare what valid input looks like and Hammer figures out the rest.
Think of it like LEGO blocks:
- You start with tiny bricks: "parse one byte", "parse a 16-bit integer".
- You snap bricks together: "parse this, then that" (sequence).
- You add choices: "parse this or that" (choice).
- The result is a complete parser assembled from simple, testable pieces.
Every file that uses Hammer needs these headers:
#include <hammer/hammer.h> // Core API: h_parse, h_uint8, h_sequence, etc.
#include <hammer/glue.h> // Convenience macros: H_RULE, H_ARULE, H_MAKE_UINT, etc.hammer.h provides the combinator functions. glue.h provides macros that reduce boilerplate.
Every combinator function returns a pointer to an HParser. This is an opaque object that knows how to recognize a specific pattern in a byte stream.
HParser *my_parser = h_uint8(); // A parser that reads one unsigned byteYou never look inside an HParser. You just compose them and eventually hand one to h_parse().
The H_RULE macro is syntactic sugar for declaring an HParser * variable:
H_RULE(name, definition);
// expands to:
HParser *name = definition;Using H_RULE is the idiomatic way to define parsers in Hammer. It makes your grammar read like a specification:
H_RULE(stratum, h_uint8());
H_RULE(header, h_sequence(leap, version, mode, stratum, poll, precision, NULL));We use H_RULE throughout this wiki.
Hammer provides parsers for standard integer types. Each one consumes the appropriate number of bits from the input.
| Parser | Bits | Signed? | Notes |
|---|---|---|---|
h_bits(n, signed) |
n | configurable | Parse exactly n bits into a 64-bit integer token. Use n <= 64 to preserve value. |
h_int8() |
8 | yes | |
h_uint8() |
8 | no | |
h_int16() |
16 | yes | |
h_uint16() |
16 | no | |
h_int32() |
32 | yes | |
h_uint32() |
32 | no | |
h_int64() |
64 | yes | |
h_uint64() |
64 | no |
H_RULE(poll, h_int8()); // Parse a signed 8-bit integer
H_RULE(ref_id, h_uint32()); // Parse an unsigned 32-bit integer
H_RULE(leap, h_bits(2, false)); // Parse exactly 2 bits (unsigned)See these in practice in the NTP Header example.
h_bits always consumes the requested number of bits, but its result is stored as
TT_UINT or TT_SINT, so values wider than 64 bits retain only the low 64 bits.
Use h_bytes() or split the field into smaller integer parsers when parsing
wider protocol fields.
Once you've built a parser, run it with h_parse():
HParseResult *result = h_parse(parser, input_bytes, input_length);| Parameter | Type | Description |
|---|---|---|
parser |
HParser * |
The parser to run |
input_bytes |
const uint8_t * |
Pointer to the raw input data |
input_length |
size_t |
Number of bytes in the input |
h_parse() returns a pointer to an HParseResult, or NULL if the input didn't match the grammar:
HParseResult *result = h_parse(my_parser, input, input_size);
if (result) {
printf("Input accepted\n");
} else {
printf("Input rejected\n");
}The HParseResult contains an ast field (of type HParsedToken *) holding the parsed data tree, described in the next section.
When h_parse() succeeds, the result contains an abstract syntax tree (AST) made of HParsedToken nodes. Each HParsedToken is a discriminated union: it has a token_type field that tells you which member of the union holds the actual data.
| Type | Union Field | Returned By |
|---|---|---|
TT_NONE |
(none) |
h_end_p, h_optional (on failure), h_epsilon_p
|
TT_BYTES |
.bytes.token, .bytes.len
|
h_token |
TT_SINT |
.sint |
h_int8, h_int16, h_int32, h_int64, h_bits(n, true)
|
TT_UINT |
.uint |
h_uint8, h_uint16, h_uint32, h_uint64, h_bits(n, false), h_ch, h_ch_range, h_in, h_not_in
|
TT_SEQUENCE |
.seq |
h_sequence, h_many, h_many1, h_repeat_n, h_sepBy, h_sepBy1
|
HParseResult *result = h_parse(h_ch_range('a', 'z'), input, len);
if (result) {
// token_type will be TT_UINT (value 8)
printf("Type: %d\n", result->ast->token_type);
printf("Char: %c\n", (char)result->ast->uint);
}When a combinator like h_sequence or h_many produces a TT_SEQUENCE, you can walk its elements:
HParseResult *result = h_parse(my_sequence_parser, input, len);
if (result) {
const HParsedToken *seq = result->ast;
size_t count = seq->seq->used;
for (size_t i = 0; i < count; i++) {
const HParsedToken *elem = h_seq_index(seq, i);
// inspect elem->token_type, elem->uint, etc.
}
}When h_token matches, the result is TT_BYTES:
HParseResult *result = h_parse(h_token((uint8_t *)"OK", 2), input, len);
if (result && result->ast->token_type == TT_BYTES) {
const uint8_t *data = result->ast->bytes.token;
size_t data_len = result->ast->bytes.len;
}Tip: Use
h_pprint(stdout, result->ast, 0, 4)to dump the entire parse tree during development. This is invaluable for understanding nested structures.
h_int_range (covered in the NTP Header example) is actually a special case of a more general mechanism: h_attr_bool, which lets you attach any boolean predicate to a parser.
| Parameter | Type | Description |
|---|---|---|
parser |
HParser * |
The parser to validate |
predicate |
bool (*)(HParseResult *, void *) |
A function that returns true to accept, false to reject |
user_data |
void * |
Optional data passed to the predicate (usually NULL) |
If parser succeeds, the result is passed to predicate. If predicate returns true, h_attr_bool succeeds with the original result. If false, the parse fails.
bool validate_checksum(HParseResult *p, void *user_data) {
const HParsedToken *seq = p->ast;
uint8_t data = h_seq_index(seq, 0)->uint;
uint8_t checksum = h_seq_index(seq, 1)->uint;
return checksum == (data ^ 0xFF);
}
H_RULE(validated_packet,
h_attr_bool(
h_sequence(h_uint8(), h_uint8(), NULL),
validate_checksum,
NULL));The H_VRULE macro (from glue.h) reduces boilerplate. It expects a function named validate_<name>:
bool validate_packet(HParseResult *p, void *user_data) {
return h_seq_index(p->ast, 1)->uint == (h_seq_index(p->ast, 0)->uint ^ 0xFF);
}
H_VRULE(packet, h_sequence(h_uint8(), h_uint8(), NULL));This is equivalent to calling h_attr_bool(def, validate_packet, NULL).
Some data formats are recursive: an element may itself contain other elements of the same type. For example, a nested list or a tree structure.
This won't compile:
H_RULE(expr, h_choice(expr, number, NULL)); // ERROR: expr used before it's definedHammer solves this with indirect binding: you create a placeholder parser with h_indirect(), use it in your grammar, then bind it to its actual definition with h_bind_indirect().
HParser *expr = h_indirect();
H_RULE(number, h_many1(h_ch_range('0', '9')));
H_RULE(paren_expr, h_middle(h_ch('('), expr, h_ch(')')));
H_RULE(atom, h_choice(number, paren_expr, NULL));
h_bind_indirect(expr, atom);The flow:
-
h_indirect()creates an empty placeholder forexpr. -
paren_exprreferencesexpr(the placeholder). -
h_bind_indirectconnects the placeholder to the actual definition (atom).
Now expr can recursively match nested parenthesized expressions like ((42)).
Caution: Left-recursive grammars (where a rule directly references itself as its first element) will cause infinite recursion with the default PB/Packrat backend. Use
h_indirectonly for non-left-recursive definitions, or use a backend that supports left recursion.
Hammer's built-in token types (TT_UINT, TT_BYTES, etc.) cover most use cases, but you can register your own types for domain-specific data. This is useful when you want action functions to produce structured results that aren't just integers or byte strings.
static HTokenType TT_MY_STRUCT;
void init_parser(void) {
TT_MY_STRUCT = h_allocate_token_type("my_struct");
// ...
}h_allocate_token_type returns a unique HTokenType value that won't collide with built-in types or other user types, even across different libraries.
Once registered, use the TT_USER value slot (the .user field, a void *) to store your custom data:
HParsedToken *act_build_struct(const HParseResult *p, void *user_data) {
MyStruct *s = malloc(sizeof(MyStruct));
s->field_a = h_seq_index(p->ast, 0)->uint;
s->field_b = h_seq_index(p->ast, 1)->uint;
HParsedToken *tok = H_MAKE(TT_MY_STRUCT, s);
return tok;
}| Function | Description |
|---|---|
h_allocate_token_type(name) |
Register and return a new token type |
h_get_token_type_number(name) |
Look up a type by name (returns 0 if not found) |
h_get_token_type_name(type) |
Get the name string for a registered type |
Note: User-defined token types are an advanced feature. For most parsers, the built-in types and
H_MAKE_UINT/H_MAKE_SEQare sufficient.
Here's a preview of the most important combinators. Each one is covered in detail in the protocol examples and the Quick Reference.
| Combinator | Purpose | Example Usage |
|---|---|---|
h_sequence(a, b, ..., NULL) |
Match a then b then ... in order |
NTP Header |
h_many(p) |
Match p zero or more times |
NTP Extensions |
h_repeat_n(p, n) |
Match p exactly n times |
NTP Hex Preprocessing |
| Combinator | Purpose | Example Usage |
|---|---|---|
h_choice(a, b, ..., NULL) |
Try a; if it fails, try b; ... |
NTP Assembling |
h_int_range(p, lo, hi) |
Match p only if its value is in [lo, hi]
|
NTP Header |
h_left(a, b) |
Match a then b, keep only a's result |
NTP Assembling |
h_end_p() |
Assert end of input (consumes nothing) | NTP Assembling |
| Combinator | Purpose | Example Usage |
|---|---|---|
h_token(str, len) |
Match a literal byte string | - |
h_ch(c) |
Match a single specific byte | - |
h_ch_range(lo, hi) |
Match a single byte in [lo, hi]
|
NTP Hex Preprocessing |
| Combinator | Purpose | Example Usage |
|---|---|---|
h_left(a, b) |
Match a then b, keep only a's result |
NTP Assembling |
h_right(a, b) |
Match a then b, keep only b's result |
- |
h_middle(a, b, c) |
Match a, b, c; keep only b's result |
- |
h_ignore(p) |
Match p but discard result (NULL AST) |
- |
| Combinator | Purpose | Example Usage |
|---|---|---|
h_put_value(p, name) |
Parse with p and store the result under name
|
NTP Extensions |
h_free_value(name) |
Retrieve a stored value (and free the binding) | NTP Extensions |
h_length_value(len, p) |
Use len's result as a count, then parse p that many times |
NTP Extensions |
h_action(p, fn, user_data) |
Parse with p, then transform the result with fn
|
NTP Extensions |
h_attr_bool(p, pred, data) |
Parse with p, fail unless pred returns true |
Hammer Fundamentals |
h_indirect() / h_bind_indirect()
|
Forward-declare a parser for recursive grammars | Hammer Fundamentals |
Every Hammer-based program follows this pattern:
1. DEFINE small parsers for individual fields (h_uint8, h_bits, ...)
2. COMPOSE them into larger structures (h_sequence, h_choice, ...)
3. CONSTRAIN with validation rules (h_int_range, h_end_p, ...)
4. RUN the top-level parser on raw input (h_parse)
5. CHECK the result (NULL = failure)
Pick a protocol example and see these concepts in action:
- NTP (Network Time Protocol) - A complete walkthrough from beginner to intermediate Hammer usage.
Or browse the full list at the Examples Index.
For testing and advanced topics, see:
-
Unit Testing - Test your parsers with
gliband Hammer'stest_suite.h. - Extending Hammer - Add new combinators, backends, or language bindings.
Previous: Getting Started
Learn Hammer
Protocol Examples
NTP
- NTP Overview
- Parsing the Header
- Parsing Data Fields
- Extension Fields and MAC
- Assembling the Parser
- Hex Input Preprocessing
- Running and Testing
DNS
TFTP
- TFTP Overview
- RRQ/WRQ Packets
- DATA Packets
- ACK Packets
- ERROR Packets
- Assembling the Parser
- Running and Testing
References
- Hammer Quick Reference
- Parsing Backends
- Unit Testing
- Using RTEMS
- Extending Hammer
- Adding a New Example
- Adding a New Binding
Further Reading