<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>First Light on PHP Boy Scout</title><link>https://phpboyscout.uk/categories/first-light/</link><description>Recent content in First Light on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Sun, 28 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/categories/first-light/index.xml" rel="self" type="application/rss+xml"/><item><title>Introducing afmpeg and ffmpeg-wasi: FFmpeg with no install, no CGO, no disk</title><link>https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/</link><pubDate>Sun, 28 Jun 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/</guid><description>&lt;img src="https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/cover-introducing-afmpeg-and-ffmpeg-wasi.png" alt="Featured image of post Introducing afmpeg and ffmpeg-wasi: FFmpeg with no install, no CGO, no disk" /&gt;&lt;p&gt;&lt;a class="link" href="https://keryx.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;keryx&lt;/a&gt; renders short promo reels, and the way it does that, today, is the way nearly everything does: it shells out to the &lt;code&gt;ffmpeg&lt;/code&gt; binary. Which is fine, until you ask it to render a project that doesn&amp;rsquo;t exist on disk. keryx can work on an in-memory project, a repo cloned straight into RAM with no local checkout, and the moment it tries to hand that to &lt;code&gt;ffmpeg&lt;/code&gt;, the whole thing falls over. The binary wants real files in a real directory. There aren&amp;rsquo;t any.&lt;/p&gt;
&lt;p&gt;I went looking for a way out and didn&amp;rsquo;t find one I could live with. The bindings that use &lt;code&gt;purego&lt;/code&gt;/&lt;code&gt;dlopen&lt;/code&gt; are immature and still need the host&amp;rsquo;s &lt;code&gt;libav&lt;/code&gt; libraries installed. The CGO bindings to &lt;code&gt;libav&lt;/code&gt; are mature and can absolutely work in memory, but they&amp;rsquo;re CGO, and CGO takes away the thing I most want from a Go program: a clean static cross-compile to a single binary that runs anywhere. The famous &lt;code&gt;ffmpeg.wasm&lt;/code&gt; is an &lt;em&gt;emscripten&lt;/em&gt; build aimed at the browser, which is the opposite target from a server-side Go tool. And the one existing WASI-capable build pins FFmpeg 5.1, which is end-of-life, and I&amp;rsquo;m not shipping an out-of-support media decoder whose entire job is parsing untrusted files. Every road had a tollbooth I wasn&amp;rsquo;t willing to pay.&lt;/p&gt;
&lt;p&gt;So I built two new things instead, and because they&amp;rsquo;re so closely tied I&amp;rsquo;m introducing them together.&lt;/p&gt;
&lt;h2 id="ffmpeg-wasi-current-ffmpeg-sandboxed-cgo-free"&gt;ffmpeg-wasi: current FFmpeg, sandboxed, CGO-free
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ffmpeg-wasi.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;ffmpeg-wasi&lt;/a&gt; (&lt;a class="link" href="https://gitlab.com/phpboyscout/ffmpeg-wasi" target="_blank" rel="noopener"
 &gt;repo&lt;/a&gt;) is the foundation, and it&amp;rsquo;s the harder of the two to build. It takes FFmpeg&amp;rsquo;s media libraries, the &lt;code&gt;libav*&lt;/code&gt; family, and builds them to &lt;code&gt;wasm32-wasi&lt;/code&gt;, then drives them with a small purpose-built engine, producing a single &lt;code&gt;.wasm&lt;/code&gt; artifact that runs anywhere a WASI runtime does. No native FFmpeg install, no C toolchain at deploy time, no shelling out to a binary. It&amp;rsquo;s built to run under &lt;a class="link" href="https://wazero.io/" target="_blank" rel="noopener"
 &gt;wazero&lt;/a&gt;, the zero-dependency pure-Go WebAssembly runtime, so a Go program can transcode, filter and mux media embedded, sandboxed, and CGO-free, still cross-compiling to one static binary.&lt;/p&gt;
&lt;p&gt;The interesting bit, the reason this didn&amp;rsquo;t already exist, is a wall that FFmpeg 7.0 put up. The 7.x series rewrote the &lt;em&gt;command-line tool&lt;/em&gt; to be mandatorily multithreaded, and a pure-Go WASI runtime can&amp;rsquo;t run that, because the threading model it needs (&lt;code&gt;wasi-threads&lt;/code&gt;, spawning real threads) isn&amp;rsquo;t something wazero does. Every project trying to get &lt;em&gt;current&lt;/em&gt; FFmpeg into WASI hits that wall, which is exactly why the existing build froze at the last single-threaded CLI, 5.1, and went EOL there. ffmpeg-wasi goes under the wall instead of over it: it doesn&amp;rsquo;t compile the CLI at all. It links the &lt;code&gt;libav*&lt;/code&gt; &lt;em&gt;libraries&lt;/em&gt; directly, which build single-threaded without complaint, and drives them with its own engine. That&amp;rsquo;s the move nobody else has made, and it&amp;rsquo;s the whole reason this can track current, maintained FFmpeg rather than a frozen one.&lt;/p&gt;
&lt;h2 id="afmpeg-the-pure-go-binding-that-lives-in-memory"&gt;afmpeg: the pure-Go binding that lives in memory
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://afmpeg.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;afmpeg&lt;/a&gt; (&lt;a class="link" href="https://gitlab.com/phpboyscout/afmpeg" target="_blank" rel="noopener"
 &gt;repo&lt;/a&gt;) was the catalyst for all this, and it&amp;rsquo;s the part a Go developer actually touches. It&amp;rsquo;s a small, idiomatic Go API (&lt;code&gt;New&lt;/code&gt;, &lt;code&gt;Run&lt;/code&gt;, &lt;code&gt;Probe&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;) sitting on top of the ffmpeg-wasi artifact, with one important twist: its I/O is bridged to an &lt;a class="link" href="https://github.com/spf13/afero" target="_blank" rel="noopener"
 &gt;&lt;code&gt;afero.Fs&lt;/code&gt;&lt;/a&gt;. afero isn&amp;rsquo;t in the standard library, but it&amp;rsquo;s the filesystem abstraction a great deal of Go already reaches for when it wants to swap a real disk for something else, and that &amp;ldquo;something else&amp;rdquo; is exactly the point here. The inputs and outputs of a media job can live entirely in memory, or in any afero backend you like, and &lt;code&gt;ffmpeg&lt;/code&gt; is none the wiser. keryx gets to render its in-memory project without ever touching the disk, which was the whole reason I started.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t a roadmap post. Both projects are released and run today: afmpeg is at &lt;code&gt;v0.4.0&lt;/code&gt;, ffmpeg-wasi at &lt;code&gt;n8.1.2-1&lt;/code&gt; (current FFmpeg, not the EOL 5.1), and between them they do real in-memory transcodes, verified end to end, WAV to AAC and H.264 rescaled and re-encoded with x264. Stripped of keryx&amp;rsquo;s reel-specific filtergraph, the shape a caller actually deals with is about this small:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Compile the ffmpeg-wasi module once, then reuse the runtime.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithModuleFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ffmpeg-wasi-lgpl.wasm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// The whole job lives in memory: no temp dir, nothing on disk.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewMemMapFs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;afero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;in.wav&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;o644&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Drive it with the ffmpeg arguments you already know...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;-i&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;in.wav&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;-c:a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;aac&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;out.m4a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ExitCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;render failed: %w (%s)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...then read the finished file straight back out of memory.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;out.m4a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;.wasm&lt;/code&gt; itself is a published release artifact you pin by SHA-256 (or let afmpeg fetch and verify for you with &lt;code&gt;WithModuleURL&lt;/code&gt;), so the licence boundary stays explicit and nothing surprising ends up in your binary.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t do everything yet, and I&amp;rsquo;d rather say so than let you find out the hard way. Single input to single output and &lt;code&gt;Probe&lt;/code&gt; work now; the full multi-pad &lt;code&gt;filter_complex&lt;/code&gt; and multi-output muxing are the next thing on the bench. But the part that nobody had cracked, getting current FFmpeg to run sandboxed, pure-Go, over a virtual filesystem with nothing on disk, is done, and you can pull it today.&lt;/p&gt;
&lt;h2 id="why-its-two-repos"&gt;Why it&amp;rsquo;s two repos
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a reason these are separate projects rather than one, and it&amp;rsquo;s about licences, not tidiness. The moment you compile FFmpeg you&amp;rsquo;re handling LGPL and GPL code, and I wanted that boundary to be obvious rather than smeared across one repo where nobody&amp;rsquo;s quite sure what&amp;rsquo;s covered by what. So the build tooling and the &lt;code&gt;(L)GPL&lt;/code&gt; &lt;code&gt;.wasm&lt;/code&gt; artifacts live in ffmpeg-wasi, with no grey areas, and afmpeg stays a clean permissive layer on top of the published artifact. I&amp;rsquo;m also shipping both LGPL and GPL builds of the artifact, so anyone who just wants the output and doesn&amp;rsquo;t fancy doing their own FFmpeg build can pick the licence that suits them and get on with it.&lt;/p&gt;
&lt;p&gt;Both repos are public, so you can rebuild or relink either one yourself, and the doc sites are now linked in the nav up top.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a stack of stories behind these two already, the threading wall and the spike that found the way under it, the afero-to-WASI filesystem bridge, the day I built a reel-shaped API and then reverted it before merge for being too narrow, the licence decision in full. I&amp;rsquo;ll be writing those up as the work lands. For now this is the headline: I needed an FFmpeg I could embed in a Go program with no install, no CGO, and no disk, couldn&amp;rsquo;t buy one off the shelf, so I built it in the open, and as of today it&amp;rsquo;s there to pull.&lt;/p&gt;</description></item><item><title>I built my wife a judge</title><link>https://phpboyscout.uk/i-built-my-wife-a-judge/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/i-built-my-wife-a-judge/</guid><description>&lt;img src="https://phpboyscout.uk/i-built-my-wife-a-judge/cover-i-built-my-wife-a-judge.png" alt="Featured image of post I built my wife a judge" /&gt;&lt;p&gt;The morning after Hailey shot a wedding, we were both wrecked, lying in bed and not really moving, and she said she might just pay for &lt;a class="link" href="https://aftershoot.com" target="_blank" rel="noopener"
 &gt;Aftershoot&lt;/a&gt; to get through the cull. And my ears pricked up.&lt;/p&gt;
&lt;p&gt;She&amp;rsquo;d been on her feet for the best part of fourteen hours the day before. She had four thousand photos waiting for her. And I, lying there next to her, realised that for the first time the thing I do in my spare time, the obsession that mostly just pays my half of the bills, might actually be of some use to her.&lt;/p&gt;
&lt;h2 id="four-thousand-photos-and-the-weeks-that-follow"&gt;Four thousand photos and the weeks that follow
&lt;/h2&gt;&lt;p&gt;A bit of background. My wife is a photographer. Not full-time yet, but that&amp;rsquo;s the plan, and she&amp;rsquo;s been chipping away at it around life and the bills ever since she set up &lt;a class="link" href="https://www.instagram.com/echos_photographyuk/" target="_blank" rel="noopener"
 &gt;Echos Photography&lt;/a&gt; a couple of years ago. Her real love is landscape and animals, the patient stuff, but every so often she&amp;rsquo;ll take on a wedding or some portraits, and last weekend she was the photographer for a family friend&amp;rsquo;s big day. A lovely affair. She shot it; I flew the drone for a bit of aerial photography and video, which mostly means I got to stand in a field feeling useful while she did the actual work.&lt;/p&gt;
&lt;p&gt;Then it ends, and the real job starts. Four thousand frames, and before a single one gets edited somebody has to sit and look at every one and decide which are even worth keeping. The blinks, the soft ones, the same moment fired off eight times in a burst where only one frame is sharp. Done by hand, in Lightroom, one at a time, that can take her weeks. Months, sometimes, depending on what else life is throwing at her. So when she said she&amp;rsquo;d pay a subscription to make the first pass go away, I understood completely. It&amp;rsquo;s a genuinely grim job.&lt;/p&gt;
&lt;p&gt;I just thought I could do better&amp;hellip; for her, specifically.&lt;/p&gt;
&lt;h2 id="why-my-ears-pricked-up"&gt;Why my ears pricked up
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been building developer tools for a while now. &lt;a class="link" href="https://gtb.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;go-tool-base&lt;/a&gt; has reached the point where it&amp;rsquo;s actually pleasant to build things on, and the work I&amp;rsquo;d been doing on keryx, a little studio you drive from a browser rather than a command line, had shown me the shape of something a non-developer could use without ever touching a terminal. The pieces were sitting there&amp;hellip; they just needed pointing at a different problem.&lt;/p&gt;
&lt;p&gt;So I built her a judge. That&amp;rsquo;s what the name means, as it happens. krites is Ancient Greek for &amp;ldquo;the judge&amp;rdquo;, and that&amp;rsquo;s the whole product in one word: the thing that looks at every frame and rules on it, keep, maybe, reject, with its reasons, so the human doesn&amp;rsquo;t have to do the first exhausting pass by hand. It judges; she still decides. It proposes, she disposes. The tool never gets the last word, and it was never meant to.&lt;/p&gt;
&lt;h2 id="a-tool-thats-only-trying-to-please-one-person"&gt;A tool that&amp;rsquo;s only trying to please one person
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing an off-the-shelf product can never quite be: krites is opinionated entirely in Hailey&amp;rsquo;s favour. It&amp;rsquo;s built around her workflow, her kit, her idea of what makes a frame worth keeping, and it&amp;rsquo;s designed so that she can use it with no setup and no technical knowledge whatsoever. The day it asks her to edit a config file is the day I&amp;rsquo;ve failed.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also kept it deliberately dim. There&amp;rsquo;s very little machine learning in it, and that&amp;rsquo;s a choice rather than a limitation. Most of the first pass, deciding whether a photo is in focus or hopelessly blown out, whether it&amp;rsquo;s the eighth near-identical frame in a burst, turns out to be arithmetic, and arithmetic is simple, fast and predictable in a way a model never quite is. I&amp;rsquo;ll reach for ML where there&amp;rsquo;s genuinely no other way (telling whether someone&amp;rsquo;s mid-blink is the obvious one), and when I do it&amp;rsquo;ll lean on the Apple hardware she already owns, with a deliberate gap left so it can grow onto other machines later. But the rule is: no cleverness she doesn&amp;rsquo;t need.&lt;/p&gt;
&lt;p&gt;And it all runs on her own machine, on purpose. No subscription, which matters when the photography isn&amp;rsquo;t paying for itself yet. No reliance on a connection, which matters because we&amp;rsquo;re hoping to spend a good chunk of the near future on the road (there&amp;rsquo;s a whole vanlife plan brewing, with a &lt;a class="link" href="https://shutter-and-stove-ec1680.gitlab.io" target="_blank" rel="noopener"
 &gt;work-in-progress demo site&lt;/a&gt; slowly coming together), and a campsite in the middle of nowhere is exactly where you want to be culling a backlog and exactly where you won&amp;rsquo;t have signal. And nothing leaves the laptop, which matters most of all, because these are other people&amp;rsquo;s weddings. Their photos shouldn&amp;rsquo;t have to go and live on somebody&amp;rsquo;s cloud to be useful to the woman they hired.&lt;/p&gt;
&lt;h2 id="why-not-just-pay-for-the-thing-that-exists"&gt;Why not just pay for the thing that exists
&lt;/h2&gt;&lt;p&gt;Aftershoot is good. Genuinely good. The easy version of this post is the one where I sneer at the incumbent, and I won&amp;rsquo;t: they&amp;rsquo;ve built something clever, and plenty of photographers are well served by it.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a product, and a product is a compromise by definition. It&amp;rsquo;s built for the average of everyone, and it can only ever evolve in the directions a company decides are profitable. Hailey isn&amp;rsquo;t the average of everyone. Something I build is hers, and it can grow in any direction she can imagine, including the unprofitable, niche, only-makes-sense-for-one-photographer directions that no company would ever green-light. That&amp;rsquo;s not really a technical argument. It&amp;rsquo;s closer to pride. I&amp;rsquo;d rather build my wife the tool than rent her someone else&amp;rsquo;s.&lt;/p&gt;
&lt;h2 id="early-days-and-the-people-who-made-it-possible"&gt;Early days, and the people who made it possible
&lt;/h2&gt;&lt;p&gt;A word on where this actually is, though. What&amp;rsquo;s shipped today is the cull, the first pass I&amp;rsquo;ve described. The straightening, the cropping, the colour work, the little retouches that eat her evenings, the part where it learns her taste over time, that&amp;rsquo;s all roadmap, and I&amp;rsquo;ll write about each piece as it lands. krites is at the very start of its life.&lt;/p&gt;
&lt;p&gt;None of it would exist at all without three things, and only one of them is mine. go-tool-base finally being solid enough to stand on. Being able to build, with a lot of AI assistance, far faster than I ever could have managed alone, fast enough that &amp;ldquo;I could probably build that&amp;rdquo; became &amp;ldquo;I have built that&amp;rdquo; inside a weekend. And Hailey, whose infinite patience and gracious kindness in indulging my passions is the only reason any of my obsessions ever get the room to become something.&lt;/p&gt;
&lt;p&gt;Which is the part I keep coming back to. I&amp;rsquo;ve &lt;a class="link" href="https://phpboyscout.uk/why-i-still-write-code/" &gt;written before&lt;/a&gt; about the cost of the compulsion to build, the evenings it takes and the people it takes them from. krites is the same compulsion, for once, pointed the other way. The thing that usually pulls me away from her, aimed squarely at giving her her evenings back. I don&amp;rsquo;t think I&amp;rsquo;ve enjoyed building anything more.&lt;/p&gt;
&lt;p&gt;She still gets the final say on every photo. Obviously. She&amp;rsquo;s the judge that matters. I just built her a faster one to argue with.&lt;/p&gt;</description></item><item><title>rust-tool-base: the same idea, in a language that argues back</title><link>https://phpboyscout.uk/rust-tool-base-the-same-idea/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/rust-tool-base-the-same-idea/</guid><description>&lt;img src="https://phpboyscout.uk/rust-tool-base-the-same-idea/cover-rust-tool-base-the-same-idea.png" alt="Featured image of post rust-tool-base: the same idea, in a language that argues back" /&gt;&lt;p&gt;I built &lt;a class="link" href="https://phpboyscout.uk/introducing-go-tool-base/" &gt;go-tool-base&lt;/a&gt; because I was sick of rebuilding the same CLI scaffolding every time I started a new Go tool. You&amp;rsquo;d think that would have taught me a lesson about doing things more than once. Apparently not, because I&amp;rsquo;ve now started building rust-tool-base: the same idea, the same itch, for Rust.&lt;/p&gt;
&lt;p&gt;In my defence, there&amp;rsquo;s method in it.&lt;/p&gt;
&lt;h2 id="the-same-itch-a-different-language"&gt;The same itch, a different language
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I kept writing the same couple of hundred lines of wiring every time I started a new Go CLI. Config loading, logging setup, an update check, an error path, a help system. None of it was the tool. All of it had to be there before the tool could be.&lt;/p&gt;
&lt;p&gt;Lately I&amp;rsquo;ve been learning Rust, and two things collided. The first is how I tend to learn a language. I&amp;rsquo;ve always picked them up reasonably quickly, and the way I do it isn&amp;rsquo;t with a tutorial that builds a toy, it&amp;rsquo;s by rebuilding something whose shape I already know cold, so that every decision is about &lt;em&gt;the language&lt;/em&gt; rather than &lt;em&gt;the problem&lt;/em&gt;. The second is that every time I started a Rust CLI of any size, I hit the very same gap I&amp;rsquo;d already filled once in Go.&lt;/p&gt;
&lt;p&gt;So rather than learn Rust on a throwaway, I decided to learn it by building rust-tool-base: the same idea, the same niche, for Rust.&lt;/p&gt;
&lt;p&gt;One housekeeping note before the series gets going. You don&amp;rsquo;t need to write Rust to follow it. The posts lean on a handful of language ideas, and rather than stop and re-explain each one mid-flow, I&amp;rsquo;ve gathered them into &lt;a class="link" href="https://phpboyscout.uk/just-enough-rust-to-follow-along/" &gt;a short primer&lt;/a&gt;. If a bit of syntax trips you up along the way, that&amp;rsquo;s where to look. If you already write Rust, ignore me and read on.&lt;/p&gt;
&lt;h2 id="the-gap-in-rust"&gt;The gap in Rust
&lt;/h2&gt;&lt;p&gt;The Rust ecosystem has a well-earned reputation for sharp, focused crates and a deliberate shortage of big opinionated frameworks. &lt;code&gt;clap&lt;/code&gt; for argument parsing, &lt;code&gt;figment&lt;/code&gt; for layered config, &lt;code&gt;tracing&lt;/code&gt; for logging, &lt;code&gt;miette&lt;/code&gt; for errors, &lt;code&gt;ratatui&lt;/code&gt; for terminal UI, &lt;code&gt;reqwest&lt;/code&gt; and &lt;code&gt;tokio&lt;/code&gt; underneath. Each of them is genuinely best-in-class.&lt;/p&gt;
&lt;p&gt;What nobody hands you is the assembly. Wiring those into one coherent product, and then adding self-update, AI integration, an MCP server, embedded documentation, credential handling, telemetry and a scaffolder, is real work, and it&amp;rsquo;s the same work on every project.&lt;/p&gt;
&lt;p&gt;The closest existing neighbours stop short of it. &lt;code&gt;cli-batteries&lt;/code&gt; is a thin preamble: argument parsing plus a logging subscriber plus panic and signal handling. &lt;code&gt;starbase&lt;/code&gt; has a proper session and lifecycle model but is CLI-agnostic and shaped around the moonrepo tooling it came from. &lt;code&gt;cargo-dist&lt;/code&gt; and &lt;code&gt;cargo-release&lt;/code&gt; are about release packaging, not the runtime. Good tools, all of them, but none is the opinionated, full-lifecycle, scaffolded base that go-tool-base is in the Go world. That space is empty, and rust-tool-base is built to fill it.&lt;/p&gt;
&lt;h2 id="why-it-is-not-a-port"&gt;Why it is not a port
&lt;/h2&gt;&lt;p&gt;The obvious way to build this would be to open go-tool-base and translate it file by file. I&amp;rsquo;m not doing that, and the reason matters enough that it&amp;rsquo;s the rule the whole project is built around.&lt;/p&gt;
&lt;p&gt;go-tool-base is full of Go. It leans on a &lt;a class="link" href="https://phpboyscout.uk/props-the-container-that-does-the-heavy-lifting/" &gt;&lt;code&gt;Props&lt;/code&gt; struct&lt;/a&gt; that carries the framework&amp;rsquo;s services in loosely-typed fields. It configures things with functional options. It registers commands using package-level &lt;code&gt;init()&lt;/code&gt;. It threads a &lt;code&gt;context.Context&lt;/code&gt; through every call. Those are all good, idiomatic Go. Transliterated into Rust they&amp;rsquo;d become code that argues with the compiler on every single line, because Rust has its own answers to every one of those problems and they are emphatically not the Go answers.&lt;/p&gt;
&lt;p&gt;So rust-tool-base reaches the &lt;em&gt;same outcomes&lt;/em&gt; by Rust&amp;rsquo;s means. Commands still self-register, but through link-time machinery instead of &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s still one context object per command, but it&amp;rsquo;s strongly typed rather than a loosely-keyed bag. Configuration is still layered, but it lands in your own typed struct instead of a string-keyed lookup. Same philosophy, same shape of product, an entirely different ecosystem underneath. The &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/README.md#L9" target="_blank" rel="noopener"
 &gt;README&lt;/a&gt; says it plainly: it&amp;rsquo;s a sibling, not a port.&lt;/p&gt;
&lt;h2 id="why-do-it-twice-at-all"&gt;Why do it twice at all
&lt;/h2&gt;&lt;p&gt;Three reasons, and they reinforce each other.&lt;/p&gt;
&lt;p&gt;The first is plain usefulness. The next time I want a Rust CLI tool, I want the same head start go-tool-base already gives me in Go.&lt;/p&gt;
&lt;p&gt;The second is the learning. Rebuilding a system I understand forces me to meet Rust&amp;rsquo;s idioms where they actually bite, not where a tutorial gently stages them. You learn ownership properly when a real design is pushing back at you.&lt;/p&gt;
&lt;p&gt;The third is the one I didn&amp;rsquo;t expect, and it&amp;rsquo;s the subject of the next post. Building the same framework twice, in two languages, turns out to be the cleanest way to find out which of your original decisions were genuine &lt;em&gt;design&lt;/em&gt; and which were merely &lt;em&gt;idiom&lt;/em&gt;. The design survives the move. The idiom does not. Sorting one from the other has been the most interesting part so far.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;rust-tool-base is the Rust sibling of go-tool-base: the same batteries-included, scaffolded, opinionated CLI framework, aimed at the same gap, which in Rust is the gap between a pile of excellent crates and a coherent product.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a port. Transliterating Go idioms into Rust produces code that fights the language, so RTB reaches the same outcomes through Rust&amp;rsquo;s own mechanisms instead. The posts after this one walk through the specific cases: how commands register, how the builder works, how errors are reported, and a few things RTB can do that the Go version structurally can&amp;rsquo;t. First, though, the thing the exercise taught me about my own design.&lt;/p&gt;</description></item><item><title>go-tool-base: I got tired of reinventing the wheel</title><link>https://phpboyscout.uk/introducing-go-tool-base/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/introducing-go-tool-base/</guid><description>&lt;img src="https://phpboyscout.uk/introducing-go-tool-base/cover-introducing-go-tool-base.png" alt="Featured image of post go-tool-base: I got tired of reinventing the wheel" /&gt;&lt;p&gt;If you&amp;rsquo;ve written more than two or three command-line tools in Go, you&amp;rsquo;ll recognise the shape of the first afternoon. I certainly do! You reach for &lt;a class="link" href="https://github.com/spf13/cobra" target="_blank" rel="noopener"
 &gt;Cobra&lt;/a&gt; for the command tree, &lt;a class="link" href="https://github.com/spf13/viper" target="_blank" rel="noopener"
 &gt;Viper&lt;/a&gt; for config, and then you start the part nobody ever puts in the README&amp;hellip; the plumbing.&lt;/p&gt;
&lt;p&gt;Where does config live? A file, an env var, an embedded default? In what order do they override each other? How does the tool tell the user there&amp;rsquo;s a newer version, and how does it actually update itself? What does logging look like, and is it the same logging the next tool will use? And how do you wire all of that into each command without every command reaching into a pile of globals?&lt;/p&gt;
&lt;p&gt;None of it is hard. That&amp;rsquo;s the problem! It&amp;rsquo;s not hard, it&amp;rsquo;s just &lt;em&gt;there&lt;/em&gt;, every single time, and every single time I&amp;rsquo;d find myself reinventing it slightly differently to the last time. Different override precedence here. A subtly different update flow there. Logging that didn&amp;rsquo;t quite match the tool I&amp;rsquo;d written three months earlier. Each new tool was a fresh re-litigation of decisions I&amp;rsquo;d already made and then promptly forgotten.&lt;/p&gt;
&lt;p&gt;Now, I&amp;rsquo;ve banged on about the Boy Scout rule for years (leave the codebase better than you found it), but it has an uncomfortable corollary. If you keep turning up to the same campsite and finding it in the same mess, at some point the honest thing to do is to stop tidying it and go and build a better campsite.&lt;/p&gt;
&lt;h2 id="first-just-packages"&gt;First, just packages
&lt;/h2&gt;&lt;p&gt;So I started pulling the recurring pieces out into their own packages. Nothing grand. A config package that did the hierarchical merge the way I always ended up doing it anyway. A version package that knew how to compare semver and spot a development build. A setup package that handled first-run bootstrap and self-updating from a release. They lived as separate repos, and if you go digging through my GitHub history you can still find the scruffy ancestors of them scattered about.&lt;/p&gt;
&lt;p&gt;Separate packages was the right &lt;em&gt;first&lt;/em&gt; move. It forced each piece to stand on its own and earn its keep on a real project before I trusted it on the next one. A package that&amp;rsquo;s only ever been used in the repo it was born in hasn&amp;rsquo;t really been tested&amp;hellip; it&amp;rsquo;s just been agreed with.&lt;/p&gt;
&lt;p&gt;But separate packages come with a tax. Each one has its own release cadence, its own changelog, its own CI. Worse, they have to agree with each other at the seams, and when they&amp;rsquo;re versioned independently those seams drift. I&amp;rsquo;d bump the config package, and the setup package that depended on it would quietly need a matching bump, and the tool that used both would need telling about both. I&amp;rsquo;d traded &amp;ldquo;reinvent the wheel&amp;rdquo; for &amp;ldquo;keep a dozen wheels in sync&amp;rdquo;, and I&amp;rsquo;m really not convinced that&amp;rsquo;s a better deal.&lt;/p&gt;
&lt;h2 id="then-one-library"&gt;Then, one library
&lt;/h2&gt;&lt;p&gt;Once the packages had been used enough (used in anger, on real tools, by people who weren&amp;rsquo;t me) the shape of them stopped moving. The interfaces settled. The arguments about precedence and defaults were over, because the answers had survived contact with reality.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the point where separate packages stop being a virtue and start being friction. So I forged them into one and called it &lt;strong&gt;go-tool-base&lt;/strong&gt;. One module, one version number, one changelog, and one set of seams that are now internal and can&amp;rsquo;t drift, because they ship together.&lt;/p&gt;
&lt;p&gt;The heart of it is a dependency-injection container, a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/props/props.go#L15" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Props&lt;/code&gt; struct&lt;/a&gt;, that holds the things every command needs: the logger, the config, the embedded assets, the filesystem handle, the error handler, the tool&amp;rsquo;s own metadata. Commands are handed &lt;code&gt;Props&lt;/code&gt; explicitly rather than reaching for globals, which means a command is just a function of its inputs and is therefore trivially testable. That one decision has quietly paid for itself on every tool I&amp;rsquo;ve built since.&lt;/p&gt;
&lt;p&gt;Around that container sits all the stuff I was so tired of rewriting: hierarchical config, structured logging, version checking, self-update from GitHub or GitLab releases, an interactive TUI documentation browser, AI integration, service lifecycle management. A new tool inherits the lot and gets to spend its first afternoon on the thing that&amp;rsquo;s actually novel&amp;hellip; its own logic.&lt;/p&gt;
&lt;h2 id="finally-a-generator"&gt;Finally, a generator
&lt;/h2&gt;&lt;p&gt;A library still leaves you staring at a blank &lt;code&gt;main.go&lt;/code&gt;. You still have to know the conventions, wire the container, lay out the directories, register the commands. All knowable, but all boilerplate. And boilerplate is exactly the enemy I set out to kill in the first place.&lt;/p&gt;
&lt;p&gt;So go-tool-base ships a generator. &lt;code&gt;gtb generate project&lt;/code&gt; scaffolds a complete, working, idiomatic project: directory layout, the wired &lt;code&gt;Props&lt;/code&gt; container, the command tree, CI, the whole lot. &lt;code&gt;gtb generate command&lt;/code&gt; adds a new command and registers it for you. The generator also handles upkeep: when the framework&amp;rsquo;s conventions move, it can regenerate the scaffolding of an existing project without trampling all over the code you&amp;rsquo;ve written on top. (That last bit turned out to be a properly interesting problem in its own right, and a future post.)&lt;/p&gt;
&lt;p&gt;The goal is blunt. Creating a CLI tool should be about the tool, not the scaffolding. The first afternoon should be spent on the part that&amp;rsquo;s actually worth writing.&lt;/p&gt;
&lt;h2 id="one-thing-i-was-careful-about"&gt;One thing I was careful about
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a nasty failure mode with &amp;ldquo;batteries-included&amp;rdquo; frameworks: the day you outgrow them, they hold you hostage. You either stay inside the framework&amp;rsquo;s worldview forever, or you face a rewrite. I&amp;rsquo;ve been burned by that before and I had no intention of inflicting it on anyone else.&lt;/p&gt;
&lt;p&gt;So go-tool-base generates idiomatic, standard-library-compliant Go. There&amp;rsquo;s no magic runtime you can&amp;rsquo;t see, no clever code you couldn&amp;rsquo;t have written by hand. If you ever outgrow the framework the generated code stands on its own and you walk away with a perfectly normal Go project. A framework should be a starting point you&amp;rsquo;re glad you took, not a room you can&amp;rsquo;t get out of.&lt;/p&gt;
&lt;h2 id="where-this-leaves-me"&gt;Where this leaves me
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I was spending the first afternoon of every Go CLI tool rebuilding the same plumbing, and rebuilding it slightly wrong relative to last time. It started life as separate packages so each piece could earn its place on real projects; once they&amp;rsquo;d stopped moving I forged them into a single library so the seams couldn&amp;rsquo;t drift; and then I wrapped a generator around it so a new tool starts as a working project rather than a blank file.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a framework for the boring 80% (config, versioning, updates, logging, lifecycle) so you can spend your time on the 20% that&amp;rsquo;s actually yours.&lt;/p&gt;
&lt;p&gt;Over the coming posts I&amp;rsquo;ll dig into the individual pieces&amp;hellip; the generator that won&amp;rsquo;t clobber your edits, the credential handling, the self-update integrity checks, and a few Go techniques I&amp;rsquo;m rather pleased with along the way. Stay tuned!&lt;/p&gt;</description></item><item><title>Introducing ZuQ - A Simple ZeroMQ Queuing Daemon</title><link>https://phpboyscout.uk/introducing-zuq/</link><pubDate>Tue, 19 Mar 2013 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/introducing-zuq/</guid><description>&lt;p&gt;We recently had the need to create a queuing system to replace an implementation of RabbitMQ that was being used on a previous project. The reasoning behind this is that the requirements of the project required a very custom implementation of a queuing system that would drastically alter in architecture as the project grew and RabbitMQ just wasn&amp;rsquo;t going to fit the bill. However to start with we required something super simple and efficient that could be expanded and developed as required. After a little investigation and a lot of recommendation from others we decided to use ZeroMQ as our transport layer for that very reason, as we could build something which could span across multiple servers and was fast.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/clientQueue.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="This is a simple Queue and Client Diagram" class="gallery-image" data-flex-basis="1082px" data-flex-grow="450" height="165" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://phpboyscout.uk/introducing-zuq/clientQueue_hu_92d5713887561406.webp" srcset="https://phpboyscout.uk/introducing-zuq/clientQueue_hu_bd9b1a72028807c9.webp 480w, https://phpboyscout.uk/introducing-zuq/clientQueue_hu_7e260053240111b3.webp 720w, https://phpboyscout.uk/introducing-zuq/clientQueue_hu_92d5713887561406.webp 744w" width="744"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The diagram above helps describe our basic queuing system. We have a queue daemon that is continuously listening for connections and two clients, one that populates the queue and the other that retrieves from the queue.&lt;/p&gt;
&lt;h2 id="the-clients"&gt;The Clients
&lt;/h2&gt;&lt;p&gt;Each client is written in PHP and uses a 0mq socket to communicate with a service, in this case our queue service. We used a SOCKET_REQ type of socket in order to have a request/response communication with our queue service.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;function client_socket(\ZMQContext $context)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // SOCKET_REQ used to create a client that sends requests to and receive from a service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client = new \ZMQSocket($context,\ZMQ::SOCKET_REQ);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client-&amp;gt;connect(&amp;#34;tcp://localhost:5555&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // SOCKOPT_LINGER = 0 Configure socket to not wait at close time
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client-&amp;gt;setSockOpt(\ZMQ::SOCKOPT_LINGER, 0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return $client;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;public function injectIntoQueue()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $context = new \ZMQContext();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $client = $this-&amp;gt;client_socket($context);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $msg = &amp;#34;This is a message&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $retries_left = 3;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $read = $write = array();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  while ($retries_left) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    // We send a request, then we wait to get a reply
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        $client-&amp;gt;send($msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        $expect_reply = true;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        while ($expect_reply) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        // Poll socket for a reply, with timeout
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $poll = new \ZMQPoll();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $poll-&amp;gt;add($client, \ZMQ::POLL_IN);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $events = $poll-&amp;gt;poll($read, $write, 2500);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            // If we got a reply, process it
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            if ($events &amp;gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            // We got a reply from the server, must match sequence
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $reply = $client-&amp;gt;recv();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                if (intval($reply) == $msg) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $retries_left = 0;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                    $expect_reply = false;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;             } elseif (--$retries_left == 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            break;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;             } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;               // Old socket will be confused; close it and open a new one
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $client = $this-&amp;gt;client_socket($context);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                // Send request again, on new socket
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $client-&amp;gt;send($msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;You can see from the code above, have a 3 strike rule. The reasoning behind this is that if the client fails to connect to the queue service more than 3 times, we can stop trying to inject into the queue and move on to the next item. As we ultimately intend to adapt the &lt;a class="link" href="http://zguide.zeromq.org/page:all#Client-side-Reliability-Lazy-Pirate-Pattern" title="lazy pirate pattern"
 target="_blank" rel="noopener"
 &gt;lazy pirate pattern&lt;/a&gt; we have made it so that if the socket times out, we can then create a new socket and retry. Without this, as the architecture becomes more complicated we may then end up in a situation where we might have errors, thus the recommend solution is to create a new socket. Once the client has sent its message to the queue, we poll for a response (i.e. which is the message we sent returned back). Once we have a response that is valid, meaning that the queue has been populated, we can stop polling until the next message.&lt;/p&gt;
&lt;p&gt;The Frontend Client&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protected function getFromQueue()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $context = new \ZMQContext();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker = new \ZMQSocket($context, \ZMQ::SOCKET_REQ);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $read = $write = array();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Set random identity to make tracing easier
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker-&amp;gt;connect(&amp;#34;tcp://localhost:5556&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Tell queue we&amp;#39;re ready for work
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker-&amp;gt;send(&amp;#34;ready&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $reply = $worker-&amp;gt;recv();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return $reply;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;Our Frontend Client is much simpler as it is part of a process that is being continually updated, therefore it doesn&amp;rsquo;t need the same connection retries are the Backend Client. We simply send &amp;ldquo;ready&amp;rdquo; to the queue system and if the queue is populated it will return us the first item.&lt;/p&gt;
&lt;h2 id="the-queuing-service"&gt;The Queuing Service
&lt;/h2&gt;&lt;p&gt;This is a continuously running executable created using c++.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::context_t context(1);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::socket_t frontend (context, ZMQ_ROUTER);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::socket_t backend (context, ZMQ_ROUTER);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backend.bind(&amp;#34;tcp://*:5555&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;frontend.bind(&amp;#34;tcp://*:5556&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;We create two sockets of type ZMQ_ROUTER which is an advanced pattern used for extending request/reply sockets. This means when we improve our queuing system we will be able to route packets to specific recipients using an address in the message.&lt;/p&gt;
&lt;p&gt;After creating our sockets, we initialise them&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// Initialize poll set
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::pollitem_t items [] = {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { frontend, 0, ZMQ_POLLIN, 0 },
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { backend, 0, ZMQ_POLLIN, 0 }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;//poll the sockets - this seems to poll both sockets at the same time
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::poll (items, 2, -1);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h2 id="backend-handler"&gt;Backend Handler
&lt;/h2&gt;&lt;p&gt;If we get a message from the backend, we check the contents to see if it contains purge at which point we empty the queue, otherwise we push the msg contents onto the queue. Finally we send the message back to the backend to show that the message has been received.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;//receive msg from client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if (items [1].revents &amp;amp; ZMQ_POLLIN) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //get message from client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zmq::message_t message(0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string client_addr = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string empty = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; assert (empty.size() == 0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string msg = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //allow the backend to purge the queue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(msg == &amp;#34;purge&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; while (!queue.empty()) queue.pop();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queue.push(msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //send response back to the backend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (backend, client_addr);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (backend, &amp;#34;&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_send (backend, msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="frontend-handler"&gt;Frontend Handler
&lt;/h2&gt;&lt;p&gt;If we get the &amp;ldquo;ready&amp;rdquo; message from the frontend client, we pop a message off the queue and return it to the frontend client. If the queue is empty we send an &amp;ldquo;empty&amp;rdquo; message back instead.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// Handle activity on frontend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if (items [0].revents &amp;amp; ZMQ_POLLIN) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //get message from worker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zmq::message_t message(0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string worker_addr = s_recv (frontend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string empty = s_recv (frontend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; assert (empty.size() == 0);}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string msg = s_recv (frontend); string queueMsg;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(msg == &amp;#34;ready&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(queue.size() &amp;gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queueMsg = queue.front();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queue.pop();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queueMsg = &amp;#34;empty&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //send reply to worker with contents of queue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (frontend, worker_addr);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (frontend, &amp;#34;&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_send (frontend, queueMsg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;That is our complete queuing system using ZeroMQ with PHP and C++.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;Using the above has allowed us to create a very simple in memory queue daemon that we can use to quickly pass data from one system to a another. On the whole it works well and we are looking to expand on it in the near future to increase both its functionality and scalability.&lt;/p&gt;
&lt;p&gt;You can find the queueing daemon (christened as &amp;ldquo;ZuQ&amp;rdquo;) on github @ &lt;a class="link" href="https://github.com/zucchi/ZuQ" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/ZuQ&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>