package wire
Install
dune-project
Dependency
Authors
Maintainers
Sources
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.vand assemble records withCodec - 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 withCodec.to_bytes/Codec.to_string, or encode into an existing buffer withCodec.encode - Export EverParse
.3dschemas viaEverparse.project/Everparse.write - Generate verified C artifacts via
Wire_3d.run - Generate OCaml FFI stubs via
Wire_stubswhen 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 wireAPI 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 | |
|---|---|---|
Integer types |
|
|
Bitfields |
| |
Bool |
|
|
Byte slices |
|
|
Byte arrays |
|
|
Fixed-count arrays |
| |
Byte-budgeted lists |
| |
Sized payloads |
| |
Enumerations |
| |
Field constraints |
| |
Codec preconditions |
| |
Actions |
| |
Parameters |
| |
Tagged unions |
| |
Dependent sizes |
| field references |
Custom mappings |
| -- |
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 diagramAscii.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_lenDevelopment
dune build
dune runtestThe 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
- Describing Binary Formats in OCaml -- the design rationale behind wire, with benchmarks
- EverParse -- verified parser generator from Project Everest
- 3D Language Reference -- EverParse DSL specification
Licence
ISC