package wire

  1. Overview
  2. Docs
Binary wire format DSL with EverParse 3D output

Install

dune-project
 Dependency

Authors

Maintainers

Sources

wire-1.3.0.tbz
sha256=8709cb675bb2e4b30554508c4756b20b4ec050b79b262df917ec01228888f361
sha512=86ca3baa3ad17c164b6c285b91eda969cdd515dcbad5246fd121e1ff4cf0129a010a2c88e18c144e4fb74a3e7e03cad0b9a55aa4c32728242136f37b9ed92a5c

Description

OCaml DSL for describing binary wire formats with EverParse 3D output. Define your wire format once, then use it for OCaml parsing via bytesrw or emit .3d files for verified C parser generation via EverParse.

Added to opam-repository:

README

ocaml-wire

Binary wire format DSL with EverParse 3D output.

Overview

Hand-written binary parsers in C are a recurring source of memory-safety bugs, which is why EverParse is attractive for security-critical systems: it generates C parsers with machine-checked proofs of memory safety and correctness. Its .3d schemas are written by hand, though, and a .3d file gives you a validator, not a serialiser or a codec for the language the application is written in.

Wire describes a binary format once, as an OCaml value, and derives from that single description both a zero-copy OCaml codec, for parsing and serialising, and the EverParse .3d schema that compiles to a verified C parser. Define the format, then:

  • Name reusable fields with Field.v and assemble records with Codec
  • Read and write fields in-place via staged Codec.get / Codec.set -- zero-copy, with zero per-call allocation for parameter-free immediate types (int, bool)
  • Decode records via Codec.decode, allocate validated encodings with Codec.to_bytes / Codec.to_string, or encode into an existing buffer with Codec.encode
  • Export EverParse .3d schemas via Everparse.project / Everparse.write
  • Generate verified C artifacts via Wire_3d.run
  • Generate OCaml FFI stubs via Wire_stubs when OCaml should call the C
  • Render RFC-style ASCII diagrams via Ascii.of_codec
  • Differential-test OCaml against C via Wire_diff

Install

opam install wire

API reference: wire on ocaml.org.

Quick start

open Wire

type packet = { version : int; flags : int; length : UInt16.t; tag : UInt8.t }

let f_version = Field.v "Version" (bits ~width:4 U8)
let f_flags   = Field.v "Flags"   (bits ~width:4 U8)
let f_length  = Field.v "Length"  uint16be
let f_tag     = Field.v "Tag"     uint8

(* Bind fields before the codec -- same objects used for get/set *)
let bf_version = Codec.(f_version $ (fun p -> p.version))
let bf_flags   = Codec.(f_flags   $ (fun p -> p.flags))
let bf_length  = Codec.(f_length  $ (fun p -> p.length))
let bf_tag     = Codec.(f_tag     $ (fun p -> p.tag))

let codec =
  let open Codec in
  v "Packet" (fun version flags length tag ->
      { version; flags; length; tag })
    [ bf_version; bf_flags; bf_length; bf_tag ]

Whole-buffer decoding

Bytes-backed decoders accept one leading value by default, which is convenient for framed streams and concatenated records. When the buffer should contain exactly one record, require full consumption explicitly:

let decode_packet buf = Codec.decode ~consume:`All codec buf 0
  0               1               2               3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |Version| Flags |            Length             |      Tag      |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Zero-copy field access

(* Staged for performance -- force once, reuse the closure *)
let get_version = Staged.unstage (Codec.get codec bf_version)
let set_version = Staged.unstage (Codec.set codec bf_version)

let buf =
  Codec.to_bytes codec
    { version = 1; flags = 2; length = UInt16.v 1024; tag = UInt8.zero }
let v = get_version buf 0        (* read version without allocating a record *)
let () = set_version buf 0 3     (* mutate version in place *)

Dependent sizes

let f_len  = Field.v "Length" uint16be
let f_data = Field.v "Data" (byte_array ~size:(Field.ref f_len))

EverParse 3D output

The same codec produces .3d files:

let schema = Everparse.project ~mode:`Ffi codec

let write () = Everparse.write ~mode:`Ffi ~outdir:"schemas" [ schema ]

The 3D output uses the EverParse output-types pattern: the generated C validates and, in the same pass, extracts every field via schema-prefixed extern callbacks (<Name>SetU8, <Name>SetU16BE, ...). See Consuming from C for what that means at the C level.

To turn those schemas into EverParse-generated C:

let run_3d () = Wire_3d.run ~outdir:"schemas" [ schema ]

If OCaml needs to call the generated C validators, generate FFI stubs:

let stubs () =
  Wire_stubs.generate ~schema_dir:"schemas" ~outdir:"."
    [ Wire_stubs.C codec ]

For unusual EverParse constructs that have no codec equivalent yet, use the Everparse.Raw API.

Consuming from C

Wire_3d.run emits a verified validator (<Name>.h/.c) alongside a default "plug" (<Name>_Fields.h/.c) that extracts every named field into a typed <Name>Fields struct. Link the plug, stack-allocate the struct, pass it as the context, read the members you care about.

#include "SpacePacket.h"
#include "SpacePacket_Fields.h"

static void err(const char *t, const char *f, const char *r,
                uint64_t c, uint8_t *ctx, uint8_t *i, uint64_t p) { (void)0; }

SpacePacketFields p = {0};
uint64_t consumed = SpacePacketValidateSpacePacket(
    (WIRECTX *)&p, NULL, err, buf, len, 0);
if (EverParseIsSuccess(consumed) && consumed == len) {
  printf("APID=%u SeqCount=%u\n", p.APID, p.SeqCount);
}

The raw Validate entry point accepts a valid prefix and returns its consumed position. Compare that position with len, as above, when the buffer must hold exactly one record. Wire's generated Check wrappers perform this whole-buffer check themselves.

Custom plug (hot-path optimisation)

If profiling says the field stores are hot, copy the shipped <Name>_Fields.c to your own my_plug.c, delete the cases for fields you don't need, and link your copy instead of the default. Override <Name>_ExternalTypedefs.h and <Name>_Fields.h in your include path if you also want a smaller WIRECTX struct; skip the override and the default struct just carries a few unused bytes.

/* my_plug.c -- started from SpacePacket_Fields.c, trimmed to one field */
#include <stdint.h>
#include "SpacePacket_Fields.h"
#include "SpacePacket_ExternalTypedefs.h"
#include "SpacePacket_ExternalAPI.h"

void SpacePacketSetU16BE(WIRECTX *ctx, uint32_t idx, uint16_t v) {
  SpacePacketFields *f = (SpacePacketFields *)ctx;
  switch (idx) {
    case SPACEPACKET_IDX_APID: f->APID = v; break;
    default: (void)f; (void)v; break;
  }
}

If your schema uses multiple setter type families (e.g. u8 fields and u16be fields), the shipped _Fields.c defines one function per family. Your copy keeps all of those functions -- delete cases, not whole functions. A family you don't care about reduces to a function whose switch has no real cases, just the default. Usually one or two short one-liners.

No weak symbols, no linker magic: whichever plug .c you link gets used.

Features

Wire covers the binary-format constructs that project cleanly to 3D. Each row is one construct, the OCaml that describes it, and the 3D it generates:

Feature

OCaml

EverParse 3D

Integer types

uint8, uint16be, uint32be, uint64be

UINT8, UINT16BE, ...

Bitfields

bits ~width:4 U32be

UINT32BE Flags : 4;

Bool

bit (bits ~width:1 U16be)

UINT16BE SYN : 1;

Byte slices

byte_slice ~size:(Field.ref f_len) (zero-copy from bytes)

UINT8 Data[:byte-size Len];

Byte arrays

byte_array ~size:(Field.ref f_len) (copied)

UINT8 Data[:byte-size Len];

Fixed-count arrays

array ~len:(int 3) uint32be

UINT32BE Items[:byte-size (3 * 4)];

Byte-budgeted lists

Field.repeat ~size:(Field.ref f_len) uint16be

UINT16BE Items[:byte-size Len];

Sized payloads

nested ~size:(Field.ref f_len) (codec inner)

Inner Body[:byte-size-single-element-array Len];

Enumerations

enum, variants

enum

Field constraints

where, Field.v ~constraint_

UINT32BE Age { Age >= 21 };

Codec preconditions

Codec.v ~where

where bound <= 1729

Actions

Action.assign, abort, if_

{:on-success ... }

Parameters

Param.input / Param.output

typedef struct _T (UINT32 bound)

Tagged unions

casetype

casetype

Dependent sizes

Field.ref f_len

field references

Custom mappings

map ~decode ~encode

--

Two distinctions the syntax makes and the OCaml names blur. A 3D array is a byte budget, never an element count, so array ~len:n multiplies the count by the element width and needs elements of a fixed size, whereas nested ~size:e is the single-element form and lowers to a different suffix entirely. And 3D's where clause is a precondition on a type's parameters, checked before any field is read; the field-level check that Wire.where and ~constraint_ mean is the unnamed { ... } refinement, which the manual files under Constraints.

Only integer tags lower to a 3D casetype. A tag of another type lowers to the tag bytes followed by a rest-of-buffer body, so the generated C validator checks framing but leaves dispatch to its caller; the OCaml decoder still rejects an unknown tag without a default case. That body must be the final field of its struct.

Real-world examples

The examples/ directory has complete definitions for CCSDS space packets and TCP/IP headers. The fragments below give the flavour.

Diagrams from the codec

The diagrams below are not hand-drawn. Ascii.of_codec renders any codec as a 32-bit-wide bit layout in the conventions of RFC 791: a two-row bit ruler, one row per 32 bits, and each field sized by the bits it actually occupies, so a diagram cannot drift from the definition the parser is built from.

let diagram = Ascii.of_codec codec
let () = print_string diagram

Ascii.pp_codec is the Format version, and of_struct / pp_struct take a Types.struct_ for a description that has no codec. A field whose width is not known until decode renders as a full-width row carrying its size expression:

 +-------------------------------+
 | Data (Len * 8 bits)           |
 +-------------------------------+

IPv4 header

let f_version  = Field.v "Version"  (bits ~width:4 U32)
let f_ihl      = Field.v "IHL"      (bits ~width:4 U32)
let f_dscp     = Field.v "DSCP"     (bits ~width:6 U32)
let f_ecn      = Field.v "ECN"      (bits ~width:2 U32)
let f_tot_len  = Field.v "TotalLen" (bits ~width:16 U32)
(* ... bound with $ inside Codec.v *)
  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |Version|  IHL  |   DSCP    |ECN|          TotalLength          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |        Identification         |Flags|       FragOffset        |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |      TTL      |   Protocol    |           Checksum            |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                            SrcAddr                            |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                            DstAddr                            |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

TCP flags (bool bitfields)

let f_syn = Field.v "SYN" (bit (bits ~width:1 U16be))
let f_ack = Field.v "ACK" (bit (bits ~width:1 U16be))

Parameters and actions

type bounded = { len : UInt16.t; data : string }

let max_len = Param.input "max_len" uint16be
let out_len = Param.output "out_len" uint16be
let f_len = Field.v "Length" uint16be
let f_data =
  Field.v "Data"
    ~action:(Action.on_success [ Action.assign out_len (Field.ref f_len) ])
    (byte_array ~size:(Field.ref f_len))

let codec =
  let open Codec in
  v "Bounded"
    ~where:Expr.(Field.ref f_len <= Param.expr max_len)
    (fun len data -> { len; data })
    [ f_len  $ (fun r -> r.len);
      f_data $ (fun r -> r.data) ]

let env = Codec.env codec |> Param.bind max_len (UInt16.v 1024)
let _ = Codec.decode ~env codec buf 0
let len = Param.get env out_len

Development

dune build
dune runtest

The benchmarks compare the OCaml codec against the EverParse-generated C and the FFI bridge, so they need 3d.exe on the PATH. The Makefile has the individual make bench-* targets.

References

Licence

ISC

Dependencies (6)

  1. re >= "1.11"
  2. optint >= "0.3"
  3. fmt >= "0.9"
  4. bytesrw >= "0.4.0"
  5. ocaml >= "5.3"
  6. dune >= "3.21"

Dev Dependencies (5)

  1. odoc with-doc
  2. mdx with-test
  3. alcobar with-test
  4. alcotest with-test
  5. memtrace with-test

Used by

None

Conflicts

None