<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Thomas Gazagnaire]]></title>
    <description><![CDATA[Building Functional Systems from Cloud to Orbit.]]></description>
    <link>https://gazagnaire.org</link>
    <lastBuildDate>Tue, 07 Apr 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://gazagnaire.org/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title><![CDATA[Predicting Satellite Collisions in OCaml]]></title>
      <description><![CDATA[<p>In January 2026, two things happened that changed how satellite
operators think about collision risk. The Office of Space Commerce
published the
<a href="https://space.commerce.gov/dataset-for-conjunction-assessment-verification/">TraCSS verification dataset</a>:
open test data, with an answer key, for anyone who wants to build
software that predicts whether two satellites will collide. A few days
later, SpaceX unveiled <a href="https://starlink.com/updates/stargaze">Stargaze</a>,
a free collision-screening service powered by 30,000 star trackers
across the Starlink constellation. In Europe,
<a href="https://www.eusst.eu/">EUSST</a> already provides conjunction screening
for over 600 satellites. All three signal the same shift: tracking
what is in orbit is becoming shared infrastructure, not a proprietary
advantage.</p>
<p>I wanted to see what it takes to build a conjunction assessment
system from scratch. I wrote an orbit propagator, a screening
pipeline, and a 3D globe to visualise the results. The screening algorithm matches the TraCSS answer key
(spherical hard-body test cases), and the whole thing runs in the
browser.</p>
<h2>Cosmos 1408</h2>
<p>On November 15, 2021, Russia destroyed the Cosmos 1408 satellite with
a missile. The debris cloud (over 1,500 trackable fragments) passed
through the ISS orbit repeatedly. The crew sheltered in their escape
capsules.</p>
<div class="my-6 not-prose" >
<canvas id="globe-ssa" ></canvas>
</div>
<p class="text-xs text-gray-500 mt-1">ISS (blue) and Cosmos 1408 debris (red) on November 15, 2021. Orbital elements are approximate (for illustration, not operational use). The animation starts 30 minutes before closest approach and runs at 60x speed. Drag to rotate, scroll to zoom.</p>
<script src="https://gazagnaire.org/blog/../js/globe_ssa.js" defer></script>
<p>This is the kind of event conjunction assessment exists to predict.
Two objects in low Earth orbit approach each other at 10-15 km/s. Ground
stations track both, but no position estimate is exact. Each one comes
with an uncertainty envelope: not &quot;the satellite is here&quot;, but &quot;the
satellite is probably within this region&quot;. Collision prediction takes
those two uncertain positions at the moment of closest approach and
computes a probability: how likely is it that the objects actually
overlap? If the probability exceeds a threshold (typically 1 in
10,000), the operator fires thrusters to move out of the way.</p>
<p>The maths for computing that probability are well understood (a 2D
integral over projected uncertainties, first published by Foster and Estes in
1992). The hard part is not the algorithm. It is knowing whether your
implementation is correct. Getting a number is easy. Trusting the
number requires open specifications you can read, and test data you
can run your code against. Until recently, neither was easy to get.</p>
<h2>Five components, one pattern</h2>
<p>A conjunction assessment system needs five things:</p>
<p><strong>Orbit propagation.</strong> Given a satellite's orbital elements (published
as Two-Line Element sets on <a href="https://www.space-track.org/">Space-Track</a>
and other catalogues), predict where it will be at any future time. The standard algorithm is
SGP4 (Spacetrack Report #3),
designed by NORAD in the 1980s and still the baseline for all
catalogued objects. Vallado's
<a href="https://arc.aiaa.org/doi/10.2514/6.2006-6753">Revisiting Spacetrack Report #3</a>
is the modern reference; the
TraCSS verification dataset
provides test vectors.</p>
<p><strong>Coordinate transforms.</strong> The propagator outputs positions in one
reference frame, the collision geometry needs another, and the
visualisation needs a third. Each conversion involves Earth rotation
models and is easy to get subtly wrong. The IAU SOFA library
documents the <a href="https://www.iausofa.org/">standard algorithms</a>;
Vallado's <em>Fundamentals of Astrodynamics and Applications</em> covers
the practical implementation.</p>
<p><strong>Message parsing.</strong> Collision warnings arrive as Conjunction Data
Messages (CDMs), defined in
<a href="https://ccsds.org/Pubs/508x0b1e2c2.pdf">CCSDS 508.0-B-1</a>.
A CDM contains the predicted time of closest approach, the miss
distance, the relative speed, the uncertainty envelopes for both
objects, and metadata about the data source. The CDM format is another
CCSDS standard, similar to the ones I described in the
<a href="https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html">wire formats</a> post. There are at least
three serialisation variants in active use (CSV from TraCSS, JSON from
Space-Track, and the CCSDS text standard).</p>
<p><strong>Probability computation.</strong> Project the two 3D uncertainty envelopes
onto the plane where collisions happen (perpendicular to the relative
velocity), then integrate the overlapping region. The result is a
single number: the probability of collision.
<a href="https://stacks.stanford.edu/file/druid:dg552pb6632/Foster-estes-parametric_analysis_of_orbital_debris_collision_probability.pdf">Foster and Estes (1992)</a>
describes the 2D method; the TraCSS dataset includes spherical test
cases with expected results for validation.</p>
<p><strong>Visualisation.</strong> Render the orbital geometry so an operator can see
what is happening. Two satellites converging at 14 km/s look identical
in a spreadsheet; on a globe, the crossing geometry is obvious. For
an operator deciding whether to manoeuvre in a window of minutes,
visual context matters.</p>
<p>The five components are standalone: I can test each one against its
own reference data before connecting them. Without Vallado's test
vectors I would not know if my propagator is correct; without the
TraCSS dataset I would not know if my probability code gives the
right answer. We learnt this in the MirageOS community when we
<a href="https://tarides.com/blog/2022-03-08-secure-virtual-messages-in-a-bottle-with-scop/">built an email parser</a>
and had to release a corpus of a million messages because nobody had
public test data. Open standards need open test data.</p>
<h2>ssa.space</h2>
<p>The result is <a href="https://ssa.space">ssa.space</a>, a conjunction assessment
dashboard. It loads the TraCSS verification dataset, propagates every
tracked object, computes the collision probability for each close
approach, and renders the results on a 3D globe. Close approaches are
colour-coded by risk: red for dangerous, orange for watch-list, green
for low. Click one to zoom in and see the orbital geometry at the
moment of closest approach.</p>
<p>This runs on test data today (the TraCSS dataset includes difficult
cases but is not live operational data). The entire stack is pure
OCaml with no system dependencies, so it runs in the browser (via
js_of_ocaml), on a server, and could run as a
<a href="https://mirageos.org/">MirageOS</a> unikernel on a satellite (the same
architecture I described in
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">the ASPLOS post</a>). That is the
direction we are pursuing with <a href="https://parsimoni.co/">SpaceOS</a>.</p>
<p>Today, collision avoidance runs through ground stations. A conjunction
warning arrives 24-72 hours before closest approach, but the ground
loop (downlink, screening, decision, command uplink) can consume most
of that window. If the satellite runs the same screening algorithm
on board, it can flag close approaches and pre-compute candidate
manoeuvres before the next ground contact. The ground still approves,
but time to first assessment drops to seconds.</p>
<h2>What comes next</h2>
<p>The application is live at <a href="https://ssa.space">ssa.space</a>. The
underlying libraries
(<a href="https://tangled.org/gazagnaire.org/ocaml-sgp4">sgp4</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-coordinate">coordinate</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-cdm">cdm</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-collision">collision</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-globe">globe</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-cam">cam</a>) are work in
progress and will be properly open-sourced when a full review is
completed (currently validating against
<a href="https://etd.gsfc.nasa.gov/capabilities/capabilities-listing/general-mission-analysis-tool-gmat/">GMAT</a>,
NASA's General Mission Analysis Tool). The immediate goal is live
data from TraCSS, Stargaze, or <a href="https://www.eusst.eu/">EUSST</a>. If
you have ephemeris data or screening results,
<a href="mailto:thomas@gazagnaire.org">get in touch</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-07-ssa.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-07-ssa.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[A CSS Engine in OCaml]]></title>
      <description><![CDATA[<p>I have rewritten my website in OCaml more times than I can count
(from <a href="https://github.com/mirage/ocaml-cow">ocaml-cow</a> to
<a href="https://github.com/Engil/Canopy">Canopy</a> to custom
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">MirageOS unikernels</a>). This time,
the styling was the problem: I use
<a href="https://tailwindcss.com/">Tailwind CSS</a>, and I wanted the entire
pipeline (Markdown to styled HTML to CSS) to be a single <code>dune build</code>
with no Node.js dependency.</p>
<p>That meant porting Tailwind's CSS generation to OCaml. To make sure
the port was correct, I needed to compare its CSS output against the
JavaScript original. The existing CSS diff tools are either abandoned or limited to CSS
2/3, and none of them handle <code>@layer</code>, container queries, nesting, or
the modern colour spaces that Tailwind v4 generates. So I started writing one. Which needed a
complete parser. Which grew into a typed AST and an optimiser, and ended up being
useful on its own.</p>
<p>The result is <a href="https://github.com/samoht/cascade">Cascade</a>, a
30,000-line OCaml library for parsing, generating, optimising, and
diffing CSS. One of the tools it ships is <code>cssdiff</code>, a structural
CSS comparison tool. Since Cascade is pure OCaml, it can be compiled to
JavaScript via <a href="https://ocsigen.org/js_of_ocaml/">js_of_ocaml</a> and
run in the browser. Try it, or pick one of the examples below:</p>
<div class="my-6 not-prose">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-xs text-gray-500 block mb-1">Left</label>
<textarea id="css-left" rows="8" class="w-full font-mono text-sm p-2 border border-gray-300 rounded-md resize-y" placeholder=".btn { color: red; }"></textarea>
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">Right</label>
<textarea id="css-right" rows="8" class="w-full font-mono text-sm p-2 border border-gray-300 rounded-md resize-y" placeholder=".btn { color: blue; }"></textarea>
</div>
</div>
<div class="flex gap-2 mt-3 items-center flex-wrap">
<button id="cssdiff-compare" class="px-4 py-1.5 text-sm bg-gray-800 text-white rounded-md hover:bg-gray-700 transition-colors cursor-pointer">Compare</button>
<span class="text-xs text-gray-500">Examples:</span>
<button onclick="loadExample('layers')" class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">@layer reorder</button>
<button onclick="loadExample('nesting')" class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">Nesting</button>
<button onclick="loadExample('container')" class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">Container queries</button>
<button onclick="loadExample('colormix')" class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">color-mix()</button>
</div>
<pre id="css-output" class="mt-3 p-3 bg-slate-800 text-slate-200 rounded-md text-xs leading-relaxed overflow-auto max-h-96 min-h-8 hidden"></pre>
</div>
<script>
var examples = {
  layers: [
    "@layer base, components, utilities;\n\n@layer base {\n  h1 { font-size: 2rem; }\n}\n@layer components {\n  .btn { padding: 0.5rem 1rem; }\n}\n@layer utilities {\n  .mt-4 { margin-top: 1rem; }\n}",
    "@layer base, utilities, components;\n\n@layer base {\n  h1 { font-size: 2rem; }\n}\n@layer components {\n  .btn { padding: 0.75rem 1.5rem; }\n}\n@layer utilities {\n  .mt-4 { margin-top: 1rem; }\n}"
  ],
  nesting: [
    ".card {\n  padding: 1rem;\n  & .title {\n    font-size: 1.5rem;\n    color: #111;\n  }\n  & .body {\n    color: #333;\n  }\n}",
    ".card {\n  padding: 1.5rem;\n  & .title {\n    font-size: 1.25rem;\n    color: #000;\n    font-weight: 600;\n  }\n}"
  ],
  container: [
    ".sidebar {\n  container-type: inline-size;\n  container-name: sidebar;\n}\n\n@container sidebar (min-width: 400px) {\n  .widget { display: grid; grid-template-columns: 1fr 1fr; }\n}",
    ".sidebar {\n  container-type: inline-size;\n  container-name: sidebar;\n}\n\n@container sidebar (min-width: 300px) {\n  .widget { display: flex; flex-direction: column; }\n}\n\n@container sidebar (min-width: 500px) {\n  .widget { display: grid; grid-template-columns: 1fr 1fr 1fr; }\n}"
  ],
  colormix: [
    ":root {\n  --brand: oklch(0.7 0.15 250);\n  --accent: color-mix(in oklch, var(--brand) 80%, white);\n}\n.btn {\n  background: var(--brand);\n  border-color: var(--accent);\n}",
    ":root {\n  --brand: oklch(0.65 0.18 260);\n  --accent: color-mix(in oklch, var(--brand) 60%, white);\n  --muted: color-mix(in oklch, var(--brand) 30%, gray);\n}\n.btn {\n  background: var(--brand);\n  border-color: var(--accent);\n  color: var(--muted);\n}"
  ]
};
function loadExample(name) {
  var e = examples[name];
  document.getElementById('css-left').value = e[0];
  document.getElementById('css-right').value = e[1];
  runCssDiff();
}
</script>
<script src="https://gazagnaire.org/blog/../js/cssdiff_js.js" defer></script>
<h2>The toolkit</h2>
<p>Cascade parses CSS strings into a typed AST, and provides a small eDSL
to build the same AST from OCaml code. From that AST, it can render
(pretty-print or minify), optimise (deduplicate and merge rules), and
structurally diff two stylesheets. The parser uses hand-written recursive descent
(does <a href="https://claude.ai/code">Claude</a> have hands?) covering
<a href="https://www.w3.org/TR/css-syntax-3/">CSS Syntax Level 3</a> through
<a href="https://www.w3.org/TR/selectors-4/">Level 4 and 5</a> (modern
selectors, colour spaces, <code>@layer</code>, container queries, nesting).</p>
<div >
<figure >
<pre><code class="language-ocaml">let css = Cascade.Css.of_string {|
  .btn {
    display: inline-block;
    background-color: #3b82f6;
    color: white;
    padding: 0.5rem;
  }
  .btn {
    background-color: #2563eb;
  }
|}
</code></pre>
<figcaption>
<p><strong>Parse.</strong> The input has a duplicate <code>.btn</code> rule (common in generated
CSS). The parser preserves both and returns a typed AST.</p>
</figcaption>
</figure>
<figure >
<pre><code class="language-ocaml">open Cascade.Css

let btn = Selector.class_ &quot;btn&quot;

let rules =
  [ rule ~selector:btn
      [ display Inline_block
      ; background_color (hex &quot;#3b82f6&quot;)
      ; color (hex &quot;#ffffff&quot;)
      ; padding (Rem 0.5) ]
  ; rule ~selector:btn
      [ background_color (hex &quot;#2563eb&quot;) ] ]
</code></pre>
<figcaption>
<p><strong>Generate.</strong> The same two rules, built from OCaml values. A typo like
<code>dsiplay</code> is a compile error. Passing a colour to <code>padding</code> is a type
error.</p>
</figcaption>
</figure>
</div>
<div >
<figure >
<pre><code class="language-css">.btn {
  display: inline-block;
  background-color: #3b82f6;
  color: #fff;
  padding: 0.5rem;
}
.btn {
  background-color: #2563eb;
}
</code></pre>
<figcaption>
<p><strong>Print.</strong> <code>Css.to_string (Css.v rules)</code> renders the AST back to CSS.
Both rules are preserved. The printer shortens <code>white</code> to <code>#fff</code>.</p>
</figcaption>
</figure>
<figure >
<pre><code class="language-css">.btn {
  display: inline-block;
  background-color: #2563eb;
  color: #fff;
  padding: 0.5rem;
}
</code></pre>
<figcaption>
<p><strong>Optimise.</strong> <code>Css.to_string ~optimize:true (Css.v rules)</code> merges the
two <code>.btn</code> rules, keeping the last <code>background-color</code> (cascade order).
It respects <code>!important</code> and preserves intentional patterns like
<code>content</code> fallbacks.</p>
</figcaption>
</figure>
</div>
<p>The <code>.btn</code> example uses a few properties, but the library has 100+
typed constructors covering layout (box model, flexbox, grid), visual
(typography, transforms, animations, filters), and logical properties.</p>
<h2>Testing against Tailwind</h2>
<p>How do you know a CSS parser is correct? The usual answer is &quot;read the
spec&quot;, and the <a href="https://github.com/web-platform-tests/wpt">W3C test suite</a>
covers parsing for individual features. But those tests check that browsers <em>apply</em> CSS correctly, not that a
tool can safely <em>transform</em> it. How do you know that merging two rules
across cascade layers or deduplicating a property does not change what
the page looks like?</p>
<p>One approach that I found works well in practice is differential
comparison against a reference implementation. For instance, to port
Tailwind to OCaml, I run the JavaScript Tailwind CLI on the same input,
then run the OCaml port, and compare the two CSS outputs byte for
byte. The comparison is strict: same characters, same order, same
whitespace. Not &quot;structurally equivalent&quot; (that would hide bugs in the
comparison tool itself).</p>
<p>When the outputs diverge (and they do, constantly, during
development), I need to know <em>which rule</em> changed and <em>how</em>. A
50,000-line string diff is unreadable. That is what <code>cssdiff</code> is
for: a structural diff that says &quot;rule <code>.mt-4</code> changed <code>margin-top</code>
from <code>1rem</code> to <code>16px</code>&quot; is actionable. It works at the AST level, so
a rule that moved from line 10 to line 200 shows as reordered, not as
a deletion plus an addition. It handles <code>@media</code>, <code>@layer</code>,
<code>@supports</code>, and <code>@container</code> blocks. This is the same tool running
in the demo above.</p>
<p>The byte-for-byte target also keeps the optimiser honest. It would be
easy to produce &quot;correct&quot; CSS that differs in harmless ways (different
property order, different hex capitalisation). But allowing those
differences means the diff tool would need to know which differences
are harmless, which is exactly the kind of subtle bug that hides
semantic errors. Matching the reference output exactly eliminates that
class of problems.</p>
<h2>Getting started</h2>
<p>Two CLI tools ship with the library: <code>cascade</code> (format, minify,
optimise CSS files) and <code>cssdiff</code> (structural comparison).</p>
<pre><code class="language-sh">brew install samoht/tap/cascade
</code></pre>
<p>Or via opam:</p>
<pre><code class="language-sh">opam pin add cascade https://github.com/samoht/cascade.git
</code></pre>
<p>The library requires OCaml 4.14 or later. The
<a href="https://github.com/samoht/cascade">README</a> has the full API overview
and CSS specification coverage table.</p>
<p>I wrote Cascade because I needed it for my blog. It turned out to be
useful beyond that: any OCaml project that generates, parses, or
compares CSS now has a typed alternative to string manipulation. The
source is on GitHub.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-02-cascade.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-02-cascade.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Describing Binary Formats in OCaml]]></title>
      <description><![CDATA[<p><a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">Satellites are becoming software platforms</a>.
More software means more bugs, and bugs 400 km overhead are not easy
to fix. Binary parsers written in C are a recurring weak spot: the
<a href="https://doi.org/10.2514/6.2022-4380">KA-SAT attack</a> bricked
thousands of satellite modems by exploiting the management interface,
and researchers have since demonstrated
<a href="https://www.usenix.org/conference/usenixsecurity24/presentation/bisping">RF signal injection</a>
against VSAT terminal firmware by crafting inputs that exploit parsing
flaws.</p>
<p><a href="https://project-everest.github.io/everparse/">EverParse</a> has a good
answer to that problem, for on-earth software. It generates C
validators and parsers with proofs of memory safety, arithmetic
safety, double-fetch freedom, and correctness. It is part of
Microsoft's <a href="https://project-everest.github.io/">Project Everest</a>,
which ran from 2016 to 2021 around
<a href="https://fstar-lang.org/">F*</a> (a dependently typed language from the ML family) and produced
software that ended up in the Windows kernel, Hyper-V, Linux,
Firefox, and Python.</p>
<p>The EverParse compiler seems like a perfect fit for embedded satellite
software. I wanted to try it, but I did not want to write
<a href="https://project-everest.github.io/everparse/3d-lang.html"><code>.3d</code></a>
files by hand (how do you maintain those? and how do you write
serialisers from them?). So I
wrote <a href="https://github.com/parsimoni-labs/ocaml-wire">ocaml-wire</a>, a
combinator library that lets me describe a format once in OCaml, automatically
write zero-allocation (when possible) parser and serialiser codecs, and generate the
<code>.3d</code> schema and C glue from the same description.</p>
<p>I will use
<a href="https://newspaceeconomy.ca/wp-content/uploads/2024/05/130x0g4e1.pdf">CCSDS Space Packets</a>
as the running example, because they are small, bitfield-heavy, and
annoying to parse by hand.</p>
<h2>EverParse</h2>
<p>For a CCSDS Space Packet header, the <code>.3d</code> format is already close to
the specification:</p>
<pre><code>module SpacePacket

typedef struct SpacePacket {
  UINT16BE Version:3;
  UINT16BE Type:1;
  UINT16BE SecHdrFlag:1;
  UINT16BE APID:11;
  UINT16BE SeqFlags:2;
  UINT16BE SeqCount:14;
  UINT16BE DataLength;
} SpacePacket;
</code></pre>
<p>You can read this as two 16-bit words of bitfields, followed by
a 16-bit <code>DataLength</code> field. EverParse turns this into a C validator
you could link into flight software. The guarantees are useful: memory safety, arithmetic safety,
double-fetch freedom, and correctness with respect to the <code>.3d</code>
specification. They do not prove that the <code>.3d</code> file matches the blue
book, nor that the C extraction from F* or the C compiler are
bug-free. Still, this is exactly the kind of low-level code where
mistakes hurt.</p>
<p>What <code>.3d</code> does not give me is the OCaml side. I would still need
separate code for decoding, encoding, and field access.</p>
<h2>The Same Header in OCaml</h2>
<p>ocaml-wire lets me
describe the same header once in OCaml, then use it directly in OCaml
and project it to EverParse <code>.3d</code>. It can also generate the OCaml/C
glue to call the verified validator. So, for the Space Packet primary
header at least, I get an OCaml codec and a C validator from the same source.</p>
<p>Define named fields with <code>Field.v</code>, bind them to record projections
with <code>$</code>, and assemble a codec with <code>Codec.v</code>:</p>
<pre><code class="language-ocaml">open Wire

type packet = {
  version : int; type_ : int; sec_hdr : int; apid : int;
  seq_flags : int; seq_count : int; data_len : int;
}

let f_version   = Field.v &quot;Version&quot;    (bits ~width:3  U16be)
let f_type      = Field.v &quot;Type&quot;       (bits ~width:1  U16be)
let f_sec_hdr   = Field.v &quot;SecHdrFlag&quot; (bits ~width:1  U16be)
let f_apid      = Field.v &quot;APID&quot;       (bits ~width:11 U16be)
let f_seq_flags = Field.v &quot;SeqFlags&quot;   (bits ~width:2  U16be)
let f_seq_count = Field.v &quot;SeqCount&quot;   (bits ~width:14 U16be)
let f_data_len  = Field.v &quot;DataLength&quot;  uint16be

(* Bind fields before building the codec. The same bound fields are then
   reused for get/set. *)
let bf_version   = Codec.(f_version   $ fun p -&gt; p.version)
let bf_type      = Codec.(f_type      $ fun p -&gt; p.type_)
let bf_sec_hdr   = Codec.(f_sec_hdr   $ fun p -&gt; p.sec_hdr)
let bf_apid      = Codec.(f_apid      $ fun p -&gt; p.apid)
let bf_seq_flags = Codec.(f_seq_flags $ fun p -&gt; p.seq_flags)
let bf_seq_count = Codec.(f_seq_count $ fun p -&gt; p.seq_count)
let bf_data_len  = Codec.(f_data_len  $ fun p -&gt; p.data_len)

let packet_codec =
  let open Codec in
  v &quot;SpacePacket&quot;
    (fun version type_ sec_hdr apid seq_flags seq_count data_len -&gt;
      { version; type_; sec_hdr; apid; seq_flags; seq_count; data_len })
    [ bf_version;
      bf_type;
      bf_sec_hdr;
      bf_apid;
      bf_seq_flags;
      bf_seq_count;
      bf_data_len ]
</code></pre>
<p>The generated <code>.3d</code> is a projection of that OCaml description. It is not
a second source of truth:</p>
<pre><code>entrypoint
typedef struct _SpacePacket(mutable WireCtx *ctx)
{
   UINT16BE Version : 3 {:act WireSetU16BE(ctx, (UINT32) 0, Version); };
   UINT16BE Type : 1 {:act WireSetU16BE(ctx, (UINT32) 1, Type); };
   UINT16BE SecHdrFlag : 1 {:act WireSetU16BE(ctx, (UINT32) 2, SecHdrFlag); };
   UINT16BE APID : 11 {:act WireSetU16BE(ctx, (UINT32) 3, APID); };
   UINT16BE SeqFlags : 2 {:act WireSetU16BE(ctx, (UINT32) 4, SeqFlags); };
   UINT16BE SeqCount : 14 {:act WireSetU16BE(ctx, (UINT32) 5, SeqCount); };
   UINT16BE DataLength {:on-success WireSetU16BE(ctx, (UINT32) 6, DataLength);
                         return true; };
} SpacePacket;
</code></pre>
<p>The annotations in the output show how EverParse uses the codec at
validation time. <code>:act</code> writes each validated field value into an
output structure (via the <code>WireSet*</code> callbacks); <code>:on-success</code> is the
longer form that can conditionally fail. So the generated C parser
validates and extracts in a single pass.</p>
<p>The same codec also gives zero-copy field access on the OCaml side:</p>
<pre><code class="language-ocaml">(* Stage once, reuse the closure *)
let get_apid = Staged.unstage (Codec.get packet_codec bf_apid)
let set_seq  = Staged.unstage (Codec.set packet_codec bf_seq_count)

let apid = get_apid buf 0        (* zero allocation for int fields *)
let () = set_seq buf 0 42        (* read-modify-write for bitfields *)

(* Full record decode/encode when needed *)
let pkt = Result.get_ok (Codec.decode packet_codec buf 0)
let () = Codec.encode packet_codec pkt buf 0
</code></pre>
<p>The Space Packet header has a fixed layout, but many formats have
variable-length payloads. For those, dependent sizes use <code>Field.ref</code>
to let one field's value determine the size of another:</p>
<pre><code class="language-ocaml">let f_len  = Field.v &quot;Length&quot; uint16be
let f_data = Field.v &quot;Data&quot; (byte_array ~size:(Field.ref f_len))
</code></pre>
<h2>Testing against C</h2>
<p>The same description drives testing against the C path:</p>
<pre><code class="language-ocaml">let schema = Everparse.schema packet_codec
let () = Wire_3d.run ~outdir:&quot;schemas&quot; [ schema ]
let () = Wire_stubs.generate ~schema_dir:&quot;schemas&quot; ~outdir:&quot;.&quot;
           [ C packet_codec ]
</code></pre>
<p><code>Wire_3d.run</code> writes the <code>.3d</code> files, invokes EverParse, and produces
C validators. <code>Wire_stubs.generate</code> writes the OCaml/C FFI glue
(external declarations and C stubs) so OCaml code can call the
verified validators directly.</p>
<p>The <code>fuzz/</code> tests use
<a href="https://github.com/stedolan/crowbar">Crowbar</a> to check that the
OCaml codec does not crash on random input. Separately, <code>test/diff/</code> generates random
schemas, runs EverParse to produce C validators, and checks that OCaml
and C agree on every random input. One OCaml description defines the
codec, the schema, and the test oracle.</p>
<h2>Performance</h2>
<p>Describing a format is one thing; the question is whether the
generated codec stays fast enough for real-time packet processing.
The benchmarks below measure three common operations (routing, frame
reassembly, status polling) and compare the Wire OCaml path with
C loops built on EverParse validators (generated from the same OCaml
codecs). The protocols come from
CCSDS
(Space Packets, TM Frames, CLCW words), which travel over
<a href="https://www.star-dundee.com/spacewire/getting-started/an-overview-of-the-spacewire-standard/">SpaceWire</a>
links at up to 200 Mbit/s.</p>
<div >
<figure >
<pre >  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
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |Versi|T|S|        APID         |Seq|         SeqCount          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |          DataLength           |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre>
<figcaption>Space Packet (6 bytes)</figcaption>
</figure>
<div>
<p><strong>Packet routing</strong> (10M variable-size packets in a 1.2 GB stream). A
satellite receives interleaved telemetry from different subsystems
(housekeeping, science, diagnostics). Each Space Packet carries an
11-bit Application Process Identifier (APID) that says which subsystem
sent it. The router reads the 6-byte header, extracts the APID, looks
up a handler in a 2048-entry table, and advances to the next packet.
This is the tightest loop in a ground station receiver.</p>
</div>
</div>
<div >
<div>
<p><strong>Status polling</strong> (1M words). The CLCW is a 32-bit status word that
the spacecraft sends back inside every telemetry frame to report the
state of its command link (locked out? waiting? retransmitting?). The
ground system polls it continuously to detect anomalies. The word
packs 13 bitfields, but the polling loop only needs 4 of them.</p>
</div>
<figure >
<pre >  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
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |C|CLC|Statu|COP|   VCID    |Spare|N|N|L|W|R|FAR|  ReportValue  |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre>
<figcaption>CLCW (32 bits)</figcaption>
</figure>
</div>
<div >
<figure >
<pre >  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
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |Ver|       SCID        |VCID |O|    MCCount    |    VCCount    |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |S|S|P|Seg|     FirstHdrPtr     |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre>
<figcaption>TM Frame (6 bytes)</figcaption>
</figure>
<div>
<p><strong>Frame reassembly</strong> (1M frames, 15M packets extracted). Telemetry
travels in fixed-size frames (1115 bytes each), but Space Packets are
variable-size and can span frame boundaries. The TM Frame header has
a FirstHdrPtr field that points to where the first complete packet
starts inside the frame. The reassembler walks each frame, extracts
the embedded packets, and stitches together packets that were split
across two frames. This is the most complex of the three benchmarks.</p>
</div>
</div>
<p>The chart below shows wall-clock time per operation (lower is better).
Each benchmark has two bars: the C baseline (blue, using EverParse
validators) and the Wire OCaml path (orange). The ratio column shows
how much slower (or faster) Wire is compared to C. A ratio below 1.0
means Wire wins.</p>
<div >
<div >
<div ></div>
<div >ns/op</div>
<div >ratio</div>
<div >Packet routing</div>
<div >
<div  title="C: 5.5 ns"></div>
<span >5.5 ns</span>
</div>
<div></div>
<div></div>
<div >
<div  title="Wire: 14.1 ns"></div>
<span >14.1 ns</span>
</div>
<div >2.6x</div>
<div >Frame reassembly</div>
<div >
<div  title="C: 64.2 ns"></div>
<span >64.2 ns</span>
</div>
<div></div>
<div></div>
<div >
<div  title="Wire: 67.4 ns"></div>
<span >67.4 ns</span>
</div>
<div >1.0x</div>
<div >Status polling</div>
<div >
<div  title="C: 6.9 ns"></div>
<span >6.9 ns</span>
</div>
<div></div>
<div></div>
<div >
<div  title="Wire: 5.2 ns"></div>
<span >5.2 ns</span>
</div>
<div >0.8x</div>
</div>
<div >
<span><span ></span> C (EverParse, -O2)</span>
<span><span ></span> Wire (OCaml 5.4.1, --profile=release)</span>
<span>Median, 10 runs, Apple M5 Max</span>
</div>
</div>
<p>Reading the chart from top to bottom: packet routing is 2.6x slower
than C, which sounds like a lot until you note that 14.1 ns per packet
gives 71 Mpkt/s (a SpaceWire link at 200 Mbit/s with 128-byte packets
carries roughly 200 kpkt/s, so Wire has 350x headroom). Frame
reassembly is at parity. And status polling is the surprise: Wire is
<em>faster</em> than C.</p>
<p>One caveat: the C baseline uses EverParse validators that extract
all fields (7 for a Space Packet, 13 for a CLCW word), while the
OCaml path uses <code>Staged.unstage (Codec.get ...)</code> to read only the
fields it needs. That makes the comparison
slightly unfair to C, but it reflects how the two paths would be used
in practice. Wire wins on CLCW precisely because it loads the 4-byte
word once and extracts only the 4 fields the polling loop needs.</p>
<p>Additional per-field micro-benchmarks confirm the pattern. Reading an 11-bit
bitfield like <code>SpacePacket.apid</code> takes 3.4 ns with zero allocation.
Writing a bitfield (read-modify-write) runs at 2–3 ns.</p>
<p>The remaining 2.5x gap in routing comes from three sources of
overhead in OCaml's runtime:</p>
<ul>
<li><strong>Function dispatch.</strong> The staged field readers are closures, not
inlined code. Each call goes through an indirect dispatch (about 11
extra instructions per call).</li>
<li><strong>Thread-safety barriers.</strong> OCaml 5 inserts memory barriers on
every mutable write to support multicore. C writes are plain stores.</li>
<li><strong>Register pressure.</strong> The compiler cannot keep state in registers
across closure calls, so it spills to the stack between reads.</li>
</ul>
<p>None of this is specific to Wire. It is the cost of OCaml 5's runtime
model. <a href="https://blog.janestreet.com/oxidizing-ocaml-locality/">OxCaml</a>
would address all three (unboxed closures, thread-local stores, better
register allocation across calls), but that is the story for another
post.</p>
<h2>Getting started</h2>
<p>To try the examples from this post, clone the repo and build:</p>
<pre><code class="language-sh">git clone https://github.com/parsimoni-labs/ocaml-wire.git
cd ocaml-wire
opam install . --deps-only
dune build
</code></pre>
<p>The library requires OCaml 5.1 or later. The codec definitions from
this post are in
<a href="https://github.com/parsimoni-labs/ocaml-wire/tree/main/examples/space">examples/space</a>
(Space Packet, CLCW, TM Frame);
<a href="https://github.com/parsimoni-labs/ocaml-wire/tree/main/examples/net">examples/net</a>
does the same for Ethernet, IPv4, TCP, and UDP. Generating <code>.3d</code>
files works out of the box; compiling them into verified C needs
<a href="https://project-everest.github.io/everparse/3d-lang.html#installing-3d">EverParse</a>
(<code>3d.exe</code> in <code>PATH</code>).</p>
<p>Satellite protocols have many binary formats: Space Packets, TM
frames, CLCW words, telecommand segments, and more. I do not want to
write C parsers and serialisers for each of them by hand. I want to
describe the format once in OCaml, get a codec I can use and compose the layers directly, and
generate the EverParse <code>.3d</code> and C glue from the same source.
<code>ocaml-wire</code> is still experimental, but I already find it quite useful
(if only to get ASCII art diagrams of packet headers :-). With OxCaml closing the remaining routing gap, the C side might matter
even less (apart from differential testing against a formally verified
parser). If you want to try it,
<a href="https://github.com/parsimoni-labs/ocaml-wire">the repo is here</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[From Cannes to Los Angeles: Visiting the People Who Build Satellites]]></title>
      <description><![CDATA[<p>Four years ago, Thales Alenia Space invited me into their integration
hall in Cannes. I was there to sketch ideas about running
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">the unikernel software we had been building at Cambridge</a>
on satellites. <a href="https://www.esa.int/Science_Exploration/Space_Science/Euclid">Euclid</a> was in the corner of the room, being prepared for
its vacuum chamber test. That kind of early trust meant a lot -- a small
team with a whiteboard, in the same building as a spacecraft about to travel
1.5 million kilometres from Earth. It led further than either of us
expected, to
<a href="https://tarides.com/blog/2023-12-29-announcing-the-orchide-project-powering-satellite-innovation/">ORCHIDE</a>
(Horizon Europe) and <a href="https://tarides.com/blog/2025-05-30-ceos-project-kick-off-using-satellites-to-survey-the-earth/">CEOS</a>
(BPI France), two major European consortium projects built around those same ideas.</p>
<p>Last month I spent a week in Southern California visiting the people who
build satellites. One morning I dropped my daughter at pre-school and walked to
the building next door, where fifteen satellites were in simultaneous
integration at different stages of assembly. That is what the LA space
industry looks like: it is just there, woven into the neighbourhood.
Another factory nearby was targeting high-volume production with a
backlog of over forty spacecraft. A space avionics manufacturer <a href="https://parsimoni.co/blog/2025-11-13-parsimoni-and-innoflight-partner-to-expand-secure-space-computing-capabilities-for-u-s-and-european-markets.html">we have since partnered with</a>,
whose hardware has flown on dozens of missions over twenty years. Engineers at <a href="https://gazagnaire.org/blog/2026-02-17-los-angeles.html">NASA/JPL</a> developing
<a href="https://gazagnaire.org/blog/2026-03-10-ocaml-fpp.html">a reusable flight software framework</a>. Teams from <a href="https://parsimoni.co/blog/2025-09-08-parsimoni-joins-techstars-space-accelerator-fall-2025-cohort.html">Techstars Space</a>
building ground segment tooling and application software that will run
on other people's satellites. All of this within an hour's drive of each other. And the
questions were different from 2022 -- not &quot;could this work?&quot; but &quot;how
big is the binary, what RTOS do you replace, how do you handle OTA updates,
what does your <a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">security compliance story</a> look like?&quot;</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-3 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/euclid-tas-cannes.jpg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/euclid-tas-cannes.jpg" alt="Euclid being integrated at Thales Alenia Space Cannes"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Euclid in the TAS Cannes integration hall (2022), about to enter the vacuum chamber. It launched in July 2023 and is now 1.5 million kilometres from Earth. ©ESA/Airbus/Thales Alenia Space.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/rocket-lab-factory.webp"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/rocket-lab-factory.webp" alt="Rocket Lab spacecraft production floor in Long Beach"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">A large satellite production line I visited last month. ©Rocket Lab.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0567.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0567.jpeg" alt="Skywriting heart over Playa Vista, Los Angeles"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Playa Vista, LA. Space + LA = ♥</figcaption>
  </figure>
</div>
<p>Launch costs are falling and production volumes are rising, and how you
produce space software at scale has to keep up. Ideas I have spent years on
(library operating systems, <a href="https://tarides.com/blog/2023-12-14-ocaml-memory-safety-and-beyond/">building software that is correct by construction</a>)
are turning out to be well-timed. I am looking forward to sending more OCaml
code to space this year. <a href="https://parsimoni.co/index.html#contact">Ping me</a>
if you are interested ;-)</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-03-12-satellite-factories.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-03-12-satellite-factories.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Apparently I Have Been Writing Flight Software All Along]]></title>
      <description><![CDATA[<p>Back in 2012, <a href="https://anil.recoil.org/">Anil Madhavapeddy</a> and I
were building MirageOS as a library OS, and we started to accumulate
a lot of libraries to manage manually. So I wrote
<a href="https://opam.ocaml.org/">opam</a> to handle the dependencies and the
<a href="https://github.com/mirage/mirage"><code>mirage</code></a> tool, an embedded DSL to describe device trees and
auto-generate the build and wiring code.
<a href="https://gabriel.radanne.net/">Gabriel Radanne</a> later refined and
<a href="https://arxiv.org/abs/1905.02529">formalised</a> it as Functoria,
which made the correspondence between device graphs and OCaml
functors explicit.
<a href="https://nasa.github.io/fprime/">F Prime</a> does the same thing for
C++ flight software: as I described in a
<a href="https://gazagnaire.org/blog/2026-02-19-nasa-fprime.html">previous post</a>, both frameworks
decompose systems into typed components and generate the plumbing
that wires them together. The difference is that Functoria is an
embedded DSL in OCaml, while
<a href="https://nasa.github.io/fpp/fpp-users-guide.html">FPP</a> (F Prime's
modelling language) is a standalone DSL with a
<a href="https://nasa.github.io/fpp/fpp-spec.html">formal specification</a>.
The interesting thing about a standalone DSL is that it can target
multiple languages: the same <code>.fpp</code> files can generate C++ and OCaml.</p>
<p>So I wrote <a href="https://github.com/parsimoni-labs/ocaml-fpp">ofpp</a> to test
this idea. It parses the complete FPP grammar and generates MirageOS
wiring code from FPP topology files. The generated code compiles
against the real MirageOS libraries, and NASA's reference <code>fpp-check</code>
tool accepts the same <code>.fpp</code> files unchanged. FPP's dependency graph
maps directly to OCaml functors, so the rest of the post walks
through that mapping.</p>
<p>ofpp is a prototype, not a replacement for the <code>mirage</code> tool or for
Functoria -- I built it to explore what a shared architecture
language between F Prime and MirageOS could look like.</p>
<h2>The mapping</h2>
<p>Here is <code>UnixHelloKey</code>, a unikernel that takes a <code>--hello</code>
command-line flag
(<a href="https://github.com/parsimoni-labs/ocaml-fpp/tree/main/examples/mirage/tutorial/hello-key">source</a>):</p>
<div >
<div >
<p><strong>FPP topology</strong></p>
<pre><code class="language-fpp">passive component Unikernel {
  async input port start: serial
  param hello: string default &quot;Hello World!&quot;
}

instance unikernel: Unikernel base id 0

topology UnixHelloKey {
  instance unikernel
}
</code></pre>
</div>
<div >
<p><strong>Generated <code>main.ml</code></strong></p>
<pre><code class="language-ocaml">let hello_0 =
  let doc = Cmdliner.Arg.info ~doc:&quot;hello&quot; [&quot;hello&quot;] in
  Mirage_runtime.register_arg
    Cmdliner.Arg.(value &amp; opt string &quot;Hello World!&quot; doc)

let start = lazy (Unikernel.start ~hello:(hello_0 ()))

let () =
  let t =
    let open Lwt.Syntax in
    (* Mirage_runtime init ... *)
    let* _ = Lazy.force start in
    Lwt.return ()
  in
  Unix_os.Main.run t; exit 0
</code></pre>
</div>
</div>
<p >The FPP instance <code>unikernel</code> resolves to the OCaml module <code>Unikernel</code> by convention (capitalised). The <code>param hello</code> declaration generates a Cmdliner term (<code>hello_0</code>) that becomes the <code>--hello</code> command-line flag, passed as a labelled argument to <code>Unikernel.start</code>.</p>
<pre><code>$ dune exec examples/mirage/tutorial/hello-key/main.exe
2026-03-04T13:52:57-08:00: [INFO] [application] Hello World!
...
$ dune exec examples/mirage/tutorial/hello-key/main.exe -- \
    --hello='Bonjour\ MirageOS!'
2026-03-04T13:53:08-08:00: [INFO] [application] Bonjour MirageOS!
...
</code></pre>
<p>ofpp handles wiring only. Type safety comes from the OCaml module
system: the generated functor applications must type-check against
the MirageOS library signatures, so a miswired topology is a compile
error.</p>
<h2>config.ml vs config.fpp</h2>
<p>In MirageOS, each unikernel has a <code>config.ml</code> that describes its
device dependencies using Functoria combinators. In FPP, each
unikernel has a <code>config.fpp</code> that describes the same dependencies
as a connection graph. Here is the <code>network</code> example -- a unikernel
that needs a TCP/IP stack:</p>
<div >
<div >
<p><strong>Functoria</strong> (<code>config.ml</code>)</p>
<pre><code class="language-ocaml">open Mirage

let main =
  main &quot;Unikernel.Main&quot; (stackv4v6 @-&gt; job)
let stack = generic_stackv4v6 default_network
let () = register &quot;network&quot; [ main $ stack ]
</code></pre>
</div>
<div >
<p><strong>FPP</strong> (<code>config.fpp</code>)</p>
<pre><code class="language-fpp">module Unikernel {
  passive component Main {
    async input port start: serial
    output port stack: serial
  }
}

instance unikernel: Unikernel.Main base id 0

topology UnixNetwork {
  import SocketStack
  instance stackv4v6
  instance unikernel

  connections Start {
    unikernel.stack -&gt; stackv4v6.connect
  }
}
</code></pre>
</div>
</div>
<p >The Functoria DSL hides the stack implementation behind <code>generic_stackv4v6</code>. The FPP topology makes it explicit: <code>import SocketStack</code> pulls in a sub-topology that wires <code>Udpv4v6_socket</code> and <code>Tcpv4v6_socket</code> into <code>Stackv4v6.Make</code>. The connection <code>unikernel.stack -> stackv4v6.connect</code> declares that the unikernel depends on the stack.</p>
<p>The FPP version is more verbose. Functoria's combinators are more
concise for pure OCaml projects, and <code>generic_stackv4v6</code> picks the
right implementation for each target automatically (FPP makes you
spell that out).
The payoff is that the topology is a standalone graph, not embedded
in OCaml. The same <code>.fpp</code> files can drive C++ code generation, and
the connection graph is available for visualisation and tooling.</p>
<h2>The device catalogue</h2>
<p>Device components live in a shared
<a href="https://github.com/parsimoni-labs/ocaml-fpp/blob/main/examples/mirage/mirage.fpp"><code>mirage.fpp</code></a>.
Here are three of the devices used in the <code>UnixPing6</code> topology
shown below:</p>
<pre><code class="language-fpp">module Vnetif {
  passive component Make {
    import Mirage_net.S
    output port backend: serial
  }
}

module Ethernet {
  passive component Make {
    import Ethernet.S
    output port net: serial
  }
}

module Ipv6 {
  passive component Make {
    import Tcpip.Ip.S
    async input port connect: Ipv6Connect
    output port net: serial
    output port eth: serial
  }
}
</code></pre>
<p>Each FPP construct maps to a specific OCaml concept.</p>
<p><strong>Output ports are functor arguments.</strong> <code>Vnetif.Make</code> has one
output port (<code>backend</code>), so its generated functor takes one
argument: <code>Vnetif.Make(Backend)</code>. <code>Ipv6.Make</code> has two (<code>net</code> and
<code>eth</code>), so its functor takes two: <code>Ipv6.Make(Net)(Ethernet)</code>. The
topology connections decide which instance fills each argument.</p>
<p><strong><code>import</code> declarations are module type constraints.</strong> <code>import Mirage_net.S</code> on <code>Vnetif.Make</code> tells ofpp to generate <code>module type Net = Mirage_net.S</code> in the output. The OCaml compiler then checks
that the concrete module satisfies that signature.</p>
<p><strong>Port types encode <code>connect</code> signatures.</strong> <code>Ipv6Connect</code> is
defined elsewhere in <code>mirage.fpp</code> as <code>port Ipv6Connect(conf: Ipv6Conf)</code>, so <code>Ipv6.connect</code> takes a labeled <code>~conf</code> argument.
Positional parameters (like <code>_0: string</code>) generate positional
arguments instead.</p>
<p>The generated code for <code>UnixPing6</code> uses all three:</p>
<pre><code class="language-ocaml">(* from import declarations *)
module type Backend = Vnetif.BACKEND
module type Net = Mirage_net.S
module type Ethernet = Ethernet.S
module type Ipv6 = Tcpip.Ip.S

(* from topology connections + output ports *)
module Net = Vnetif.Make(Backend)
module Ethernet = Ethernet.Make(Net)
module Ipv6 = Ipv6.Make(Net)(Ethernet)

(* from connect port types + connection order *)
let* backend = Backend.connect () in
let* net = Net.connect backend in
let* ethernet = Ethernet.connect net in
let* ipv6 = Ipv6.connect net ethernet in
</code></pre>
<p>If a connection is missing, it does not compile.</p>
<h2>A larger example</h2>
<p>Here is <code>UnixPing6</code>, an IPv6 ping unikernel wired through a
virtual network
(<a href="https://github.com/parsimoni-labs/ocaml-fpp/tree/main/examples/mirage/device-usage/ping6">source</a>):</p>
<div class="my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/UnixPing6.png"><img class="w-full rounded-lg" src="https://gazagnaire.org/blog/images/UnixPing6.png" alt="UnixPing6 topology"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight"><b>Figure 1.</b> The <code>UnixPing6</code> topology, rendered by <a href="https://github.com/fprime-community/fprime-visual">fprime-visual</a> from the FPP connection graph.</figcaption>
  </figure>
</div>
<div >
<div >
<p><strong>FPP topology</strong> (<code>config.fpp</code>)</p>
<pre><code class="language-fpp">module Unikernel {
  passive component Main {
    async input port start: serial
    output port net: serial
    output port eth: serial
    output port ipv6: serial
  }
}

instance unikernel: Unikernel.Main base id 0

topology UnixPing6 {
  instance backend
  instance net
  instance ethernet
  instance ipv6
  instance unikernel

  connections Connect {
    net.backend -&gt; backend.connect
    ethernet.net -&gt; net.connect
    ipv6.net -&gt; net.connect
    ipv6.eth -&gt; ethernet.connect
  }
  connections Start {
    unikernel.net -&gt; net.connect
    unikernel.eth -&gt; ethernet.connect
    unikernel.ipv6 -&gt; ipv6.connect
  }
}
</code></pre>
<p >The <code>backend</code> and <code>net</code> instances come from the shared <code>mirage.fpp</code> device catalogue (<code>Backend</code> is a Vnetif in-memory switch, <code>Vnetif.Make</code> creates a virtual Ethernet interface from it). The two connection groups (<code>Connect</code>, <code>Start</code>) become separate lazy bindings.</p>
</div>
<div >
<p><strong>Generated <code>main.ml</code></strong></p>
<pre><code class="language-ocaml">module type Backend = Vnetif.BACKEND
module type Net = Mirage_net.S
module type Ethernet = Ethernet.S
module type Ipv6 = Tcpip.Ip.S

module Net = Vnetif.Make(Backend)
module Ethernet = Ethernet.Make(Net)
module Ipv6 = Ipv6.Make(Net)(Ethernet)
module Unikernel = Unikernel.Main(Net)(Ethernet)(Ipv6)

open Lwt.Syntax

let connect = lazy (
  let* backend = Backend.connect () in
  let* net = Net.connect backend in
  let* ethernet = Ethernet.connect net in
  let* ipv6 = Ipv6.connect net ethernet in
  Lwt.return (net, ethernet, ipv6))

let start = lazy (
  let* (net, ethernet, ipv6) = Lazy.force connect in
  Unikernel.start net ethernet ipv6)

(* Mirage_runtime init, then: *)
let* _ = Lazy.force start in ...
</code></pre>
<p >ofpp reads the connection graph, sorts the dependencies, and emits the functor applications in the right order. The <code>Backend</code> → <code>Net</code> → <code>Ethernet</code> → <code>Ipv6</code> chain mirrors the physical network stack.</p>
</div>
</div>
<h2>Sub-topology composition</h2>
<p>Sub-topologies are shared via <code>import</code>. The DNS example imports the
socket stack and wires additional layers on top:</p>
<pre><code class="language-fpp">topology UnixDns {
  import SocketStack          @ pulls in Udpv4v6_socket, Tcpv4v6_socket, Stackv4v6
  instance stackv4v6
  instance happy_eyeballs_mirage
  instance dns_client
  instance unikernel

  connections Connect_device {
    happy_eyeballs_mirage.stack -&gt; stackv4v6.connect
  }
  connections Start {
    dns_client.stack -&gt; stackv4v6.connect
    dns_client.happy_eyeballs -&gt; happy_eyeballs_mirage.connect_device
    unikernel.dns -&gt; dns_client.start
  }
}
</code></pre>
<p>The generated <code>main.ml</code> chains the socket stack <code>connect</code>, then
<code>connect_device</code> for Happy Eyeballs, then <code>start</code> for the DNS
resolver -- each connection group becomes a lazy binding that
forces its dependencies:</p>
<pre><code class="language-ocaml">module type Udpv4v6_socket = Tcpip.Udp.S
module type Tcpv4v6_socket = Tcpip.Tcp.S
module type Stackv4v6 = Tcpip.Stack.V4V6
module type Happy_eyeballs_mirage = Happy_eyeballs_mirage.S
module type Dns_client = Dns_client_mirage.S

module Stackv4v6 = Stackv4v6.Make(Udpv4v6_socket)(Tcpv4v6_socket)
module Happy_eyeballs_mirage = Happy_eyeballs_mirage.Make(Stackv4v6)
module Dns_client = Dns_resolver.Make(Stackv4v6)(Happy_eyeballs_mirage)
module Unikernel = Unikernel.Make(Dns_client)

let connect = lazy (
  let* udpv4v6_socket = Udpv4v6_socket.connect ~ipv4_only:false ... in
  let* tcpv4v6_socket = Tcpv4v6_socket.connect ~ipv4_only:false ... in
  Stackv4v6.connect udpv4v6_socket tcpv4v6_socket)

let connect_device = lazy (
  let* stackv4v6 = Lazy.force connect in
  let* happy_eyeballs_mirage = Happy_eyeballs_mirage.connect_device stackv4v6 in
  Lwt.return (stackv4v6, happy_eyeballs_mirage))

let start = lazy (
  let* (stackv4v6, happy_eyeballs_mirage) = Lazy.force connect_device in
  let* dns_client = Dns_client.start stackv4v6 happy_eyeballs_mirage in
  Unikernel.start dns_client)
</code></pre>
<p >This is something Functoria's flat combinator model does not express cleanly: named sub-topologies that can be imported and reused across unikernels, with cross-boundary connections wired by the parent topology.</p>
<h2>Target switching</h2>
<p>In MirageOS, you write your application once and compile it for
different platforms without changing application code.
<code>mirage configure -t unix</code> wires in the kernel's TCP/IP stack;
<code>-t hvt</code> wires in MirageOS's own protocol implementations running on
a virtual network device. In FPP, each target is simply a different
topology.</p>
<p>Shared sub-topologies (<code>TcpipStack</code>, <code>SocketStack</code>, <code>DnsStack</code>) are
imported and reused across variants. The caller picks the target by
passing it to ofpp:</p>
<pre><code class="language-sh">ofpp to-ml --topologies UnixHello mirage.fpp tutorial/hello/config.fpp
ofpp to-ml --topologies UnixDns --target unix mirage.fpp applications/dns/config.fpp
</code></pre>
<p>The <code>--target</code> flag selects the OS main loop: <code>unix</code> (default),
<code>xen</code>, <code>solo5</code>, or <code>unikraft</code>. The codegen emits the appropriate
entry point -- <code>Unix_os.Main.run</code>, <code>Xen_os.Main.run</code>,
<code>Solo5_os.Main.run</code>, or <code>Unikraft_os.Main.run</code>. Target switching
in ofpp is very experimental for now: only the Unix target has
been tested end-to-end.</p>
<h2>Mixing C++ and OCaml components</h2>
<p>ofpp already works for building MirageOS unikernels on the Unix
backend: describe the topology in FPP, run <code>ofpp to-ml</code>, compile.
The interesting next step is mixing C++ F Prime components with
OCaml MirageOS components in the same topology.</p>
<p>Three pieces are missing. First, <strong>binary serialisation</strong>: ofpp
already generates OCaml types from FPP enums, structs, and arrays,
but matching C++'s wire format for command, telemetry, and event
dispatch is not done yet. Second, <strong>FFI stubs</strong>: I have early
experiments with OCaml components running inside F Prime over
<code>caml_callback</code>, but they are not ready to open-source. Third,
<strong>build integration</strong>: I have a working (if ugly) CMake + dune
bridge, though a cleaner solution might come from
<a href="https://ryan.freumh.org/papers/2026-package-calculus.pdf">Package Managers à la Carte</a>
(pre-print).</p>
<p>The end goal: run OCaml components on isolated MirageOS platforms
(<a href="https://github.com/Solo5/solo5">Solo5</a>,
<a href="https://muen.codelabs.ch/">Muen</a>,
<a href="https://github.com/mirage/mirage-unikraft">Unikraft</a>, seL4)
inside F Prime deployments, and let MirageOS unikernels integrate
with existing C++ flight software.</p>
<h2>State machines</h2>
<p>FPP also has state machines. ofpp generates typed OCaml modules
from them: states become a GADT, signals become a variant, and
guards and actions become functor parameters. Here is a toy example:</p>
<div >
<div >
<p><strong>FPP state machine</strong></p>
<pre><code class="language-fpp">state machine Door {
  action lock
  guard locked
  signal open
  signal close
  initial enter Closed
  state Closed {
    on open if locked enter Closed
    on open enter Opened
  }
  state Opened {
    on close do { lock } enter Closed
  }
}
</code></pre>
</div>
<div >
<p><strong>Generated OCaml</strong></p>
<pre><code class="language-ocaml">type closed
type opened

type _ state =
  | Closed : closed state
  | Opened : opened state

type signal = Open | Close

module type ACTIONS = sig
  type ctx
  val lock : ctx -&gt; unit
end

module type GUARDS = sig
  type ctx
  val locked : ctx -&gt; bool
end

module Make
  (A : ACTIONS)
  (G : GUARDS with type ctx = A.ctx) :
sig
  type t
  val create : A.ctx -&gt; t
  val state : t -&gt; any
  val step : t -&gt; signal -&gt; t
end
</code></pre>
</div>
</div>
<p >Phantom types index the GADT, so pattern matching in <code>step</code> is exhaustive. The caller supplies <code>ACTIONS</code> and <code>GUARDS</code> as functor arguments. ofpp also renders state machines as Graphviz diagrams. In F Prime, components can embed <code>state machine instance</code> declarations that connect a state machine to the component's lifecycle; ofpp does not wire those up fully yet.</p>
<h2>ofpp</h2>
<p>The tool is called
<a href="https://github.com/parsimoni-labs/ocaml-fpp">ofpp</a>. I started it
in February at the JPL workshop. Rob Bocchino
<a href="https://rob-bocchino.net/Professional/bocchino-ieee-aero-2022.pdf">designed FPP</a>
with a
formal specification
and an upstream test suite of 670 <code>.fpp</code> files across 35
categories, which I used to validate the parser. The parser covers
the complete FPP grammar (zero conflicts).</p>
<p>The whole thing is about 19k lines of OCaml: 1k for the parser and
lexer, 8k for the checker, 2.7k for the code generator, and the
rest for tests. ofpp also exports topology graphs to
<a href="https://github.com/fprime-community/fprime-visual">fprime-visual</a>.
The <a href="https://github.com/parsimoni-labs/ocaml-fpp/tree/main/examples/mirage">example repo</a>
covers all of
<a href="https://github.com/mirage/mirage-skeleton">mirage-skeleton</a>:
tutorials, device examples, and applications like DNS, DHCP, and
Git. Every example has a cram test that builds the unikernel and
exercises it.</p>
<p>ofpp is not affiliated with NASA or JPL. It is an independent
experiment. If you work with FPP or MirageOS and want to try it:</p>
<pre><code class="language-sh"># macOS
brew tap parsimoni-labs/tap &amp;&amp; brew install ofpp

# from source
opam pin add fpp https://github.com/parsimoni-labs/ocaml-fpp.git
</code></pre>
<pre><code class="language-sh">ofpp check path/to/file.fpp
ofpp to-ml path/to/file.fpp
ofpp dot path/to/file.fpp | dot -Tsvg -o sm.svg
</code></pre>
<p>If you have a MirageOS <code>config.ml</code>, try describing the same topology
in FPP and compare the generated <code>main.ml</code> with what Functoria
produces. File issues on
<a href="https://github.com/parsimoni-labs/ocaml-fpp/issues">GitHub</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-03-10-ocaml-fpp.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-03-10-ocaml-fpp.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Is Running Untrusted Code on a Satellite a Good Idea?]]></title>
      <description><![CDATA[<p>The same conversation keeps happening. I explain what
<a href="https://parsimoni.co/">Parsimoni</a> does (run third-party software
on someone else's satellite) and the response is always some
variant of: &quot;I would never trust code I have not reviewed to run
on my satellite.&quot;
They are right to worry -- I would say the same thing in their
position. And the security research community has been saying it loudly
for several years now.</p>
<p>To understand where the gaps are, it helps to separate the two
halves of the stack. Bus software (attitude control, power, thermal)
runs on qualified RTOSes and is the satellite manufacturer's domain.
Payload software (imaging, communications, data processing)
increasingly runs on COTS (commercial off-the-shelf)
processors with memory management units (MMUs) perfectly capable of enforcing memory
isolation. For LEO missions on COTS hardware, the hardware is
there. The software running on it does not use it.</p>
<p>I think three things are missing for anyone wanting to run hosted
payloads or patch software in orbit safely:</p>
<ol>
<li>
<p><strong>Hardware-enforced memory isolation</strong> between payloads and
flight software, either through a hypervisor (like Xen or KVM) or
a separation kernel (seL4 has a formal proof of functional
correctness on specific hardware configurations). Both use the
MMU to ensure a compromised payload cannot reach the bus.</p>
</li>
<li>
<p><strong>A standard OTA deployment format</strong> so &quot;install this application&quot;
means the same thing across different satellites: cryptographically
signed, designed for 10-minute LEO pass windows and partial
transfers.</p>
</li>
<li>
<p><strong>A metered runtime</strong> that enforces resource quotas (CPU, memory,
storage, bus bandwidth) so one payload cannot starve the others.
Resource requirements should be declarative, embedded in the
deployment image, and verified before installation.</p>
</li>
</ol>
<p>The tools to address much of this exist in the terrestrial world:
memory-safe languages (<a href="https://msrc.microsoft.com/blog/2019/07/we-need-a-safer-systems-programming-approach/">roughly 70% of CVEs</a>
in large C/C++ codebases are memory-safety issues, a figure consistent
across Microsoft, Google, and Android since 2019), verified cryptography,
minimal single-purpose OS images. These are not theoretical: Google
<a href="https://security.googleblog.com/2024/09/eliminating-memory-safety-vulnerabilities-Android.html">cut Android memory-safety vulnerabilities by 76%</a>
in six years by writing new code in memory-safe languages, and AWS
runs millions of serverless workloads on
<a href="https://www.usenix.org/conference/nsdi20/presentation/agache">Firecracker</a>,
a 50,000-line Rust VMM that boots in under 125 ms. None of these
have made it into flight software yet, even as the number of
spacecraft in orbit (and the attack surface) has grown sharply.
<a href="https://doi.org/10.1109/SP46215.2023.10351029">Space Odyssey</a> examined
three satellites and found vulnerabilities in all of them, and those are
only the ones someone bothered to look for.</p>
<div class="my-6 flex flex-col items-center not-prose">
  <img src="https://gazagnaire.org/blog/images/satellite-cves.svg" alt="Mass launched to orbit versus publicly disclosed satellite software vulnerabilities, 2000-2025" class="max-w-2xl w-full">
  <p class="text-sm text-gray-500 mt-2 max-w-2xl text-center">Mass launched to orbit (blue) versus publicly disclosed satellite software vulnerabilities (red bars, from CVE/NVD and conference disclosures; <a href="https://gazagnaire.org/blog/images/satellite-cves.py" class="underline">data and sources</a>). The gap between the curves is the problem: launch volume has exploded, security scrutiny has barely started.</p>
</div>
<p>The evidence is mounting. None of the Space Odyssey satellites
had ASLR, stack canaries, or DEP; one had no authentication
on its telecommand interface. The Viasat/KA-SAT attack in February
2022 disrupted Ukrainian communications and 5,800 German wind
turbines, making the threat operational (Boschetti, Gordon and Falco,
<a href="https://doi.org/10.2514/6.2022-4380">ASCEND 2022</a>). At Black Hat
2025, a
<a href="https://visionspace.com/nasa-cfs-version-aquila-software-vulnerability-assessment/">vulnerability assessment</a>
of NASA's core Flight System (cFS) found remote code execution,
path traversal, and denial-of-service, all from textbook C memory
bugs. NASA's own
<a href="https://github.com/nasa/CryptoLib/security">CryptoLib</a>, the
reference implementation of CCSDS encryption, has had recurring
buffer overflows in its parsing code
(<a href="https://github.com/nasa/CryptoLib/security/advisories/GHSA-q2pc-c3jx-3852">CVE-2025-29909</a>,
CVSS 9.8). A memory-safe language eliminates these classes of bug
by construction.</p>
<p>The pattern across all of these is consistent: space software
engineering has a safety-versus-security bias (Pavur and Martinovic,
<a href="https://doi.org/10.1093/cybsec/tyac008">J. Cybersecur. 2022</a>;
Falco, <a href="https://doi.org/10.2514/1.I010693">JAIS 2019</a>). Systems are
designed for availability and determinism (watchdogs, radiation
tolerance, FDIR). Integrity and confidentiality are secondary. This
is not irrational: a satellite that loses attitude control is a more
immediate problem than one that leaks telemetry. But it leaves
software stacks undefended against adversarial threats. Jero et al.
(<a href="https://doi.org/10.14722/spacesec.2024.23015">NDSS SpaceSec 2024</a>)
proposed a defence-in-depth taxonomy -- secure boot, memory
protection, authenticated updates, compartmentalisation -- that
overlaps substantially with the three gaps above.</p>
<p>So why does the current stack not provide the three things above?
Most small-sat payload software runs on
<a href="https://www.windriver.com/products/vxworks">VxWorks</a>,
<a href="https://www.rtems.org/">RTEMS</a>, embedded Linux
(<a href="https://www.yoctoproject.org/">Yocto</a>,
<a href="https://buildroot.org/">Buildroot</a>), or bare metal. The RTOS path
typically runs everything in a single address space: the hardware has
an MMU, but most CubeSat missions do not enable memory protection
because it requires rethinking memory management, interrupt handling,
and DMA buffer allocation. This is exactly the architecture Space
Odyssey found vulnerable. Embedded Linux gives you process isolation,
but hardening it for multi-tenancy and certifying it for flight
remains a substantial undertaking. ESA's
<a href="https://www.esa.int/Enabling_Support/Operations/OPS-SAT">OPS-SAT</a>
demonstrated deploying new applications before its re-entry in 2024,
but it remains the exception.</p>
<p>The isolation and metering techniques are not new. Cloud
infrastructure solved analogous problems years ago, and avionics
systems (ARINC 653, DO-178C) have their own qualified solutions. The
gap is in LEO payload software, where neither stack fits: the cloud
stack assumes abundant resources, the avionics stack assumes a single
operator and a multi-year qualification budget. I am not going to
minimise how much work qualification (under ECSS-Q-ST-80C or
equivalent) requires. Calabrese,
Kavallieratos and Falco
(<a href="https://doi.org/10.2514/6.2024-0270">SCITECH 2024</a>) emulated a
cyberattack from a hosted payload on OPS-SAT that compromised the
bus, demonstrating why isolation matters. But a real multi-tenant
satellite also needs FDIR (Fault Detection, Isolation, and Recovery)
integration, thermal management, power budgeting, and link budget
allocation. I am focusing on the software infrastructure, but I do
not want to pretend the problem ends there.</p>
<p>The regulatory pressure is new. The EU Cyber Resilience Act is
pushing toward stronger isolation and update mechanisms.
<a href="https://www.enisa.europa.eu/publications/space-threat-landscape">ENISA</a>
is calling out the space sector. The White House
<a href="https://tarides.com/blog/2024-03-07-a-time-for-change-our-response-to-the-white-house-cybersecurity-press-release/">called for memory-safe languages</a>
in critical infrastructure in 2024, a recommendation that applies
to flight software as much as to anything on the ground. OCaml, the
language Parsimoni and Tarides use, is memory-safe in its default
fragment. And the hardware is finally there:
radiation-tolerant processors with enough compute for on-board
processing are commercially available at CubeSat price points. The
missing piece is the software infrastructure.</p>
<p>These are the problems I co-founded
<a href="https://parsimoni.co/">Parsimoni</a> to work on. Our payload software
platform, SpaceOS, builds on the OCaml ecosystem that
<a href="https://tarides.com/blog/2023-12-14-ocaml-memory-safety-and-beyond/">Tarides maintains</a>.
Our first payload
reached orbit on SpaceX Transporter-13 in March 2025. If you are
building payload software and running into these constraints, or if
you are an operator considering hosted payloads, we would like to
hear from you: <a href="https://parsimoni.co/index.html#contact">get in touch</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-25-satellite-software.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-25-satellite-software.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[From ASPLOS to Orbit: Unikernels Twelve Years Later]]></title>
      <description><![CDATA[<p>Our 2013 ASPLOS paper,
&quot;<a href="https://doi.org/10.1145/2451116.2451167">Unikernels: Library Operating Systems for the Cloud</a>&quot;,
received a <a href="https://anil.recoil.org/notes/unikernels-test-of-time">test-of-time
award</a> in
March 2025. The title says &quot;for the Cloud.&quot; We wrote it while I was
in <a href="https://www.cl.cam.ac.uk/~jac22/">Jon Crowcroft</a>'s group at the University of Cambridge. The core
idea: instead of running an application on a general-purpose OS,
compile the application and only the OS libraries it needs into a
single sealed image that runs directly on a hypervisor. No kernel, no
shell, no unused drivers. Our DNS server image was 200 KB. The BIND
appliance it replaced was over 400 MB.</p>
<p>Five properties fell out of that architecture. None of them are
unique to unikernels (a careful Yocto configuration can achieve most
of them), but unikernels make them the default. I think they hold up
better now than they did in the cloud:</p>
<ol>
<li><strong>Sealed images.</strong> The binary is fixed at compile time. No runtime
package installation, no configuration files, no shell to log into.</li>
<li><strong>Dead code elimination.</strong> If the application does not use the
filesystem, the filesystem is not in the binary.</li>
<li><strong>Single address space.</strong> One application, one address space (no
internal process isolation, the hypervisor provides isolation
between unikernels).</li>
<li><strong>Configuration is compilation.</strong> Network addresses, TLS
certificates, and firewall rules are baked in at build time. No
configuration parser at runtime means no configuration parser to
exploit.</li>
<li><strong>Small trusted computing base.</strong> The TCB is the hypervisor plus
the application. No general-purpose kernel, no init system, no
sshd.</li>
</ol>
<div  class="my-8 not-prose">
<div >
<div >
  <div >Application</div>
  <div >System libraries</div>
  <div >Unused packages</div>
  <div >Shell, sshd, cron</div>
  <div >OS kernel</div>
  <div >Hypervisor</div>
</div>
<p >Traditional VM</p>
</div>
<div >
<div >
  <div >Application + OS libs</div>
  <div >OCaml runtime</div>
  <div >Hypervisor</div>
</div>
<p >MirageOS unikernel</p>
</div>
</div>
<p>Anil, Balraj and I co-founded Unikernel Systems to bring this to
production.
<a href="https://www.mosaicventures.com/blog/the-story-of-our-investment-in-docker-via-unikernel-systems-and-why-enterprises-are-running-their-business-on-their-commercial-platform">Docker acquired us three months later</a>.
At Docker, we built
<a href="https://www.docker.com/blog/how-docker-desktop-networking-works-under-the-hood/">Docker Desktop</a>
from scratch, adding <a href="https://mirageos.org/">MirageOS</a> unikernel
components to the networking stack
via <a href="https://github.com/moby/vpnkit">vpnkit</a> (we wrote an <a href="https://doi.org/10.1145/3747525">ICFP
paper</a> about how that works).
Millions of developers use it daily. Meanwhile, containers won the
cloud and <a href="https://unikraft.com/">Unikraft</a> took the unikernel
deployment model in a POSIX-compatible direction for serverless. The
unikernel-as-product did not take over (yet!), but the libraries shipped
further than we ever expected.</p>
<p>In 2021, when <a href="https://tarides.com/">Tarides</a> joined Cyber@Station F,
I pitched unikernels to over thirty internal teams at Thales: cyber,
compliance, transport, defence, ... Most were politely interested. The one that
clicked was Régis de Ferluc at
<a href="https://www.thalesaleniaspace.com/">Thales Alenia Space</a>. His
reaction was not &quot;that is interesting&quot; but &quot;that is how we already
build embedded systems, but done in a principled way (and where you
can safely introduce dynamic behaviour).&quot;</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-2 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/asplos25-award.jpg"><img class="w-full max-h-64 object-cover object-bottom rounded-lg" src="https://gazagnaire.org/blog/images/asplos25-award.jpg" alt="Richard Mortier accepting the ASPLOS test-of-time award in Rotterdam"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Richard Mortier accepting the ASPLOS test-of-time award in Rotterdam (2025).</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/tas.jpg"><img class="w-full max-h-64 object-cover rounded-lg" src="https://gazagnaire.org/blog/images/tas.jpg" alt="Early whiteboard session with Régis de Ferluc at TAS Cannes"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Early whiteboard session with Régis de Ferluc at TAS Cannes, evaluating MirageOS components for space (2022).</figcaption>
  </figure>
</div>
<p>He was right. Satellite flight software ships
as fixed binaries, qualified on the ground, no shell access in orbit,
no runtime configuration changes. The five properties above are not
innovations in space, they are requirements. What satellite software
does not have is the type safety, the tooling, and the library
ecosystem that <a href="https://ocaml.org/">OCaml</a> and MirageOS provide.</p>
<p>Régis pushed us to look deeper, introduced us to EU programmes
(ORCHIDE via Horizon Europe, CEOS), and
<a href="https://parsimoni.co/blog/2023-07-31-ocaml-in-space-welcome-spaceos.html">SpaceOS</a>
was born. <a href="https://www.linkedin.com/in/miklostomka/">Miklos</a> and I co-founded
<a href="https://parsimoni.co/">Parsimoni</a> to build it.</p>
<p>The ASPLOS paper asked whether sealed, single-purpose images could
improve cloud infrastructure. Twelve years later, I think the more
interesting question is whether those same properties matter more in
constrained environments (satellites, hardware security modules,
embedded controllers) where every kilobyte counts, you cannot ssh in
after deployment, and &quot;no unused code&quot; is not an optimisation but a
qualification requirement.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[F Prime Looks a Lot Like MirageOS (but in C++)]]></title>
      <description><![CDATA[<p>Last week I attended the
<a href="https://gazagnaire.org/blog/2026-02-17-los-angeles.html">F Prime workshop at JPL</a>, over 100
people, from CubeSat student teams to flagship mission engineers. I
learned a lot about F Prime, and I kept noticing how familiar the
concepts felt. This post is some of my thoughts on why.</p>
<p><a href="https://nasa.github.io/fprime/">F Prime</a> (<a href="https://github.com/nasa/fprime">GitHub</a>) is NASA's open-source
framework for building <em>reusable</em> flight software. Before it, most
missions started from scratch or copy-pasted code from previous
ones. It already flies on
<a href="https://mars.nasa.gov/technology/helicopter/">Ingenuity</a> (Mars
Helicopter),
<a href="https://www.jpl.nasa.gov/missions/cadre">CADRE</a> (autonomous lunar
rovers), and <a href="https://europa.nasa.gov/">Europa Clipper</a>. It is trying to
bring standard software engineering practices (modularity, unit
testing, reusability, continuous integration) to a domain that has
historically resisted them.</p>
<p>The framework has its own modelling language called
<a href="https://nasa.github.io/fpp/fpp-users-guide.html">FPP</a> (F Prime
Prime). You describe your system in FPP (components, ports, state
machines, topologies) and the toolchain generates thousands of lines
of C++ for you (plus a fair amount of CMake glue to wire the build
together). That is the point: you write the architecture, not the
boilerplate. If you have ever worked with MirageOS, all of this should sound
oddly familiar.</p>
<p>For those who do not know,
<a href="https://mirageos.org/">MirageOS</a> (<a href="https://github.com/mirage/mirage">GitHub</a>) is a library operating system where
the entire application, from the network stack to the storage layer,
is built from OCaml modules wired together by functors. The runtime
is minimal (Solo5 provides about ten hypercalls), no general-purpose
OS, no libc, just typed interfaces all the way down. Our
<a href="https://doi.org/10.1145/2451116.2451167">ASPLOS 2013 paper</a>
received the
<a href="https://anil.recoil.org/notes/unikernels-test-of-time">Influential Paper Award</a>
in 2025. I think F Prime and MirageOS share a surprising number of
ideas. Here is how I see the two relate.</p>
<p><strong>Components are module types.</strong> In F Prime, you define a component
by declaring its ports, typed interfaces that connect it to the
rest of the system. The FPP paper puts it nicely: a port &quot;has a type
and a kind; the type is like a function signature.&quot; Components have
no compile- or link-time dependencies on each other: each component
depends only on the types of the ports that it uses.</p>
<p>FPP distinguishes three kinds of components: passive (called
synchronously by the caller), queued (has an internal message queue
but no thread of its own), and active (has its own thread that
drains its queue). This is a concurrency model baked into the
component types:</p>
<div >
<div >
<pre><code class="language-fpp">passive component Thermometer {
  sync input port cmdIn: Fw.Cmd
  output port tlmOut: Fw.Tlm
  telemetry Temperature: F32
}
</code></pre>
<p >FPP: a passive component declares typed ports.</p>
</div>
<div >
<pre><code class="language-ocaml">module type THERMOMETER = sig
  type t
  val cmd_in : t -&gt; Cmd.t -&gt; unit
  val tlm_out : t -&gt; Tlm.t
  val temperature : t -&gt; float
end
</code></pre>
<p >OCaml: a module type declares the same interface.</p>
</div>
</div>
<p>Both say the same thing: anything that calls itself a thermometer
must accept commands, emit telemetry, and report a temperature. The
ports are the function signatures. The component type is the module
type.</p>
<p>The mapping is suggestive, not exact. FPP ports have directionality
(input vs output) and invocation kinds (sync, async, guarded) that a
flat OCaml signature does not enforce. In MirageOS we used phantom
types on device connectors to recover some of this structure (see
<a href="https://arxiv.org/abs/1905.02529">Radanne, Gazagnaire et al., 2019</a>)
but the parallel is clear enough to be useful.</p>
<p>The concurrency story is interesting too. F Prime's
active/passive/queued distinction maps loosely to what OCaml has been
working through for years. A passive component is a plain function
call. An active component has its own thread that drains a message
queue. Compare:</p>
<div >
<div >
<pre><code class="language-fpp">active component Logger {
  async input port logIn: Fw.Log
}
</code></pre>
<p >FPP: an active component has its own thread.</p>
</div>
<div >
<pre><code class="language-ocaml">module type LOGGER = sig
  type t
  val log_in : t -&gt; Log.t -&gt; unit Lwt.t
end
</code></pre>
<p >OCaml: <code>Lwt.t</code> marks the call as asynchronous.</p>
</div>
</div>
<p>In FPP, a component is either passive, queued, or active. In OCaml,
you can mix synchronous and asynchronous functions in the same module
signature: some values return <code>unit</code>, others return <code>unit Lwt.t</code>. The
concurrency model is per-function, not per-component. Queued
components sit in between, an event loop without a dedicated thread.
OCaml has been moving from monadic concurrency (<code>Lwt.t</code>) to
<a href="https://ocaml.org/manual/effects.html">effect handlers</a> in OCaml 5,
which delegate scheduling to a library
like <a href="https://github.com/ocaml-multicore/eio">Eio</a> without colouring
every type signature. F Prime bakes the concurrency model into the
component kind; OCaml delegates it to the application scheduler.</p>
<p><strong>State machines are GADTs.</strong> This one is not specific to MirageOS,
it is about OCaml's type system more broadly. FPP has built-in
state machine support (states, signals, guards, and transitions).
Here is a door controller (from the
<a href="https://nasa.github.io/fpp/fpp-users-guide.html">FPP User's Guide</a>):</p>
<div >
<div >
<pre><code class="language-fpp">state machine Door {
  signal open
  signal close
  guard isLocked

  initial enter Closed

  state Closed {
    on open if isLocked enter Closed
    on open enter Open
  }

  state Open {
    on close enter Closed
  }
}
</code></pre>
<p >FPP: a door with two states and a guard.</p>
</div>
<div >
<img src="https://gazagnaire.org/blog/images/door-sm.svg" alt="Door state machine" >
<p >The generated state machine diagram.</p>
</div>
</div>
<p>In OCaml, you can encode the same transitions as a GADT. The type
parameters track which state you are in and which state a signal
takes you to:</p>
<div >
<div >
<pre><code class="language-ocaml">type closed
type opened

type _ state =
  | Closed : closed state
  | Open : opened state

type (_, _) signal =
  | Open  : (closed, opened) signal
  | Close : (opened, closed) signal

type guard = Locked | Unlocked

let step : type a b. a state -&gt; (a, b) signal -&gt; guard
    -&gt; (b state, a state) result =
  fun state signal guard -&gt;
    match state, signal, guard with
    | Closed, Open, Locked   -&gt; Error state
    | Closed, Open, Unlocked -&gt; Ok Open
    | Open, Close, _         -&gt; Ok Closed
</code></pre>
<p >OCaml: the GADT encodes valid transitions in the types. The guard returns a result: you either advance or stay put.</p>
</div>
</div>
<p>The type <code>(closed, opened) signal</code> means: this signal is only valid
from the <code>closed</code> state, and it produces the <code>opened</code> state. Try to
close an already-closed door and the compiler rejects it. The guard
is a runtime check (just like in FPP), but the result type makes the
self-loop explicit: <code>Error state</code> means the guard blocked and you
stayed where you were. FPP's state machine compiler does the same
checks at the FPP level; the GADT pushes them into OCaml's own type
system.</p>
<p><strong>The wiring is where they diverge.</strong> How you describe the way all
these components connect into a running system is where the two
frameworks take different paths.</p>
<p>In F Prime, you define a <em>topology</em>: a
wiring diagram that connects component instances through their ports,
the way an engineer connects chips on a board. Sub-topologies let you
group and reuse partial wirings (think of them as sub-circuits). The
FPP compiler checks that port types match, then the autocoder
generates C++ and a fair amount of CMake glue. For the final assembly,
users customise init sequences, thread priorities, and buffer sizes
through C++ templates and build rules. There are many knobs to turn.</p>
<div >
<div >
<pre><code class="language-fpp">topology Mission {
  instance sensor: Sensors.Thermometer
  instance dispatcher: Svc.CmdDispatcher

  connections command {
    dispatcher.compCmdSend -&gt; sensor.cmdIn
  }
}
</code></pre>
<p >FPP: a topology wires component instances through their ports.</p>
</div>
<div >
<pre><code class="language-ocaml">module Mission
    (Sensor : THERMOMETER)
    (Dispatcher : CMD_DISPATCHER) = struct
  let dispatch sensor cmd =
    Sensor.cmd_in sensor cmd
  let init sensor =
    Dispatcher.register (dispatch sensor)
end
</code></pre>
<p >OCaml: a functor wires modules through their signatures.</p>
</div>
</div>
<p>In MirageOS, a <em>functor</em> is a function from modules to modules: you
pass a module satisfying some signature, and you get a new module
back. The functor is the sub-topology. People write functor
applications by hand all the time in OCaml, and it works fine. But
in MirageOS we wanted to automate redeployment across different
hypervisors and driver sets (Xen, KVM, Solo5, each with their own
network and storage stacks), so we built
<a href="https://arxiv.org/abs/1905.02529">Functoria</a>, an eDSL that
describes the assemblage and generates the build rules and functor
applications for each target. That is structurally closer to what
F Prime's autocoder does: both take a high-level wiring description
and produce glue code for a specific target.</p>
<p>Both check that the wiring is type-correct, but in different places:
FPP in its own compiler, OCaml in the host language's type system.
FPP gives you a graph-oriented model that maps naturally to how
electronic engineers think. Functoria gives you composability
(functors are first-class language constructs), but the resulting
code can get... involved. (Who said Irmin in the back? :p)</p>
<p>The unit of compute seems settled: small typed components, explicit
async I/O, function types as interfaces. The global wiring is not. I
have seen it spawn build system rules (Makefile spaghetti),
domain-specific languages (FPP, Functoria), and language-specific support (functors).
<a href="https://doi.org/10.1145/361598.361623">Parnas (1972)</a> told us to
hide information behind interfaces. Nobody told us how to wire a
thousand of them together ;-)</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-19-nasa-fprime.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-19-nasa-fprime.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[New Year, New Blog]]></title>
      <description><![CDATA[<p>Hello world, finally this is live :-)</p>
<p>The last couple of weeks have been pretty intense as I've been moving
to LA with my family. I am spending six months here, working on
<a href="https://parsimoni.co/">Parsimoni</a> and getting closer to the US space
industry.</p>
<p>Last week I visited NASA's <a href="https://www.jpl.nasa.gov/">Jet Propulsion Laboratory (JPL)</a> and saw
Voyager 2's control room -- still running after 48 years.
<a href="https://mars.nasa.gov/resources/26340/optimism-in-jpls-mars-yard/">Optimism</a>,
Perseverance's ground twin, was rolling over Pasadena sand in a test
enclosure next door. I also attended the
<a href="https://nasa.github.io/fprime/">F Prime</a> workshop and completed two
workshops on flight software engineering. F Prime is NASA's open-source
flight software framework. Over 100 people attended -- university and
student teams, internal JPL product teams, and large projects like
<a href="https://www.nasa.gov/game-changing-development-projects/high-performance-spaceflight-computing-hpsc/">HPSC</a>.
Only a small bunch of us were not from the US.</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-3 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0410.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0410.jpeg" alt="Charles Elachi Mission Control Center at JPL"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Charles Elachi Mission Control Center at JPL. The right screen reads VGR2 +48 YY.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0395.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0395.jpeg" alt="Optimism rover at JPL"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Optimism, Perseverance's ground twin, in the JPL test enclosure.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0344.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0344.jpeg" alt="F Prime workshop at JPL"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">The F Prime workshop at JPL.</figcaption>
  </figure>
</div>
<p>The recurring theme in every conversation: flying a helicopter on Mars
is now routine, but deploying shared payload software to a satellite is still
real science fiction (no pressure!).</p>
<p>On the <a href="https://tarides.com/">Tarides</a> side, the team continues to
maintain and develop the OCaml platform -- opam, Dune, Merlin, odoc
-- while I am on a different timezone. Lots of things are
happening (see the <a href="https://tarides.com/blog/">Tarides blog</a>) and many
more are planned for 2026. In a world where people write apps by
chatting with AI bots, core language tools are even more important to make
sure we keep trust in our software systems (more on this later in this
blog, hopefully).</p>
<p>I intend to write here more regularly this year -- about SpaceOS,
the OCaml platform, and what happens when you try to put unikernels
in orbit. Stay tuned.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-17-los-angeles.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-17-los-angeles.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate>
    </item>
  </channel>
</rss>