package wire
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=b1310eabd9a945b38b3b4dc62f91986fd9d54ee2a006a65c8dcf8c36a61563ba
sha512=60a73a9f045079223b812863aea92ac7d3bffdc17b878ff9940301a93fb2f0f86f9c1a17999fdcd9f6647e13d0f53aef958e8d59c1ca42f84645e8809b39f1ce
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
Codec.get/Codec.set-- zero-copy, zero-allocation for immediate types (int, bool) - Decode and encode records via
Codec.decode/Codec.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 : int; tag : int }
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 ] 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 = Bytes.create (Codec.wire_size codec)
let () =
Codec.encode codec { version = 1; flags = 2; length = 1024; tag = 0 } buf 0
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};
if (EverParseIsSuccess(SpacePacketValidateSpacePacket(
(WIRECTX *)&p, NULL, err, buf, len, 0))) {
printf("APID=%u SeqCount=%u\n", p.APID, p.SeqCount);
}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 |
|
|
Enumerations |
| |
Constraints |
| |
Actions |
| |
Parameters |
| |
Tagged unions |
| |
Arrays |
|
|
Dependent sizes |
| field references |
Custom mappings |
| -- |
Real-world examples
The examples/ directory has complete definitions for CCSDS space packets and TCP/IP headers. The fragments below give the flavour; Ascii.of_codec renders the diagrams shown alongside them.
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 : int; 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 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