Skip to content

Hammer Fundamentals

Elbasiouny, Mahmoud edited this page Aug 5, 2026 · 8 revisions

These are the core building blocks of the Hammer library. Everything you build with Hammer uses these primitives, regardless of the protocol.


What Is a Parser Combinator Library?

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.

Including Hammer

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.


HParser - The Parser Type

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 byte

You never look inside an HParser. You just compose them and eventually hand one to h_parse().


H_RULE - Naming Your Parsers

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.


Data Type Parsers

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

Example

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.


Running a Parser with h_parse()

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

Return Value

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.


Parse Results in Detail

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.

HTokenType Values

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

Inspecting a Simple Result

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);
}

Inspecting a Sequence

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.
    }
}

Inspecting Bytes

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.


Custom Validation with h_attr_bool

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.

h_attr_bool(parser, predicate, user_data)

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.

Example: Checksum Validation

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));

H_VRULE Shorthand

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).


Indirect Binding for Recursive Grammars

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 defined

Hammer 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().

h_indirect() / h_bind_indirect(indirect, parser)

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:

  1. h_indirect() creates an empty placeholder for expr.
  2. paren_expr references expr (the placeholder).
  3. h_bind_indirect connects 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_indirect only for non-left-recursive definitions, or use a backend that supports left recursion.


User-Defined Token Types

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.

Registering a New Type

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.

Using It in an Action Function

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;
}

Looking Up Types

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_SEQ are sufficient.


Key Combinators (Preview)

Here's a preview of the most important combinators. Each one is covered in detail in the protocol examples and the Quick Reference.

Sequencing and Structure

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

Choice and Filtering

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

Token and Character Matching

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

Structural (Discarding Results)

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) -

Data-Dependent Parsing

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

The Overall Flow

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)

Next Steps

Pick a protocol example and see these concepts in action:

Or browse the full list at the Examples Index.

For testing and advanced topics, see:

  • Unit Testing - Test your parsers with glib and Hammer's test_suite.h.
  • Extending Hammer - Add new combinators, backends, or language bindings.

Previous: Getting Started

Clone this wiki locally