<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Serjio</title>
    <description>The latest articles on DEV Community by Serjio (@serjioua).</description>
    <link>https://dev.to/serjioua</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4011265%2Fbae8c0b0-f9a2-4f09-9b55-25e22f2057d8.png</url>
      <title>DEV Community: Serjio</title>
      <link>https://dev.to/serjioua</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serjioua"/>
    <language>en</language>
    <item>
      <title>MessageFrame vs Protobuf vs JSON: Does a Schema-Free C++17 Framework Actually Win?</title>
      <dc:creator>Serjio</dc:creator>
      <pubDate>Sat, 15 Aug 2026 13:12:30 +0000</pubDate>
      <link>https://dev.to/serjioua/messageframe-vs-protobuf-vs-json-does-a-schema-free-c17-framework-actually-win-26mg</link>
      <guid>https://dev.to/serjioua/messageframe-vs-protobuf-vs-json-does-a-schema-free-c17-framework-actually-win-26mg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fari2ma4egvcy5vva5ch2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fari2ma4egvcy5vva5ch2.png" alt="MessageFrame vs Protobuf benchmark" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every serialization library claims to be fast. Benchmarking your own library against itself is easy and mostly useless. The useful test is different: take the &lt;strong&gt;same logical message&lt;/strong&gt;, build it with four different libraries, and measure the full round-trip on the same hardware.&lt;/p&gt;

&lt;p&gt;That is what this post does. &lt;a href="https://github.com/stubcpp/MessageFrame" rel="noopener noreferrer"&gt;MessageFrame&lt;/a&gt; — the schema-free C++17 messaging library I built for a multi-SDR telemetry system — goes up against &lt;strong&gt;Google Protobuf&lt;/strong&gt;, &lt;strong&gt;nlohmann/json&lt;/strong&gt;, and &lt;strong&gt;msgpack-cxx&lt;/strong&gt;. The harness is in the repository, so you can reproduce every number below in a couple of minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick recap: what MessageFrame is
&lt;/h2&gt;

&lt;p&gt;If you have not seen the earlier posts (&lt;a href="https://dev.to/serjioua/a-small-c-library-for-sending-structured-commands-and-telemetry-between-devices-no-schema-2phf"&gt;short introduction&lt;/a&gt;), here is the idea in three bullets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two-part keys, no schema, no code generation.&lt;/strong&gt; Every parameter is addressed as &lt;code&gt;(device, parameter)&lt;/code&gt; and assembled at runtime: &lt;code&gt;msg.add("sdr_1", "rx_gain", VALUE(30.0))&lt;/code&gt;. A new device appears on the network, you just start adding its parameters. No &lt;code&gt;.proto&lt;/code&gt;, no &lt;code&gt;protoc&lt;/code&gt;, no rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid storage container.&lt;/strong&gt; Up to 128 parameters (&lt;code&gt;SMALL_CAPACITY&lt;/code&gt;) everything lives in a flat, allocation-free &lt;code&gt;std::vector&lt;/code&gt; with excellent cache behavior. Past that it migrates transparently to a &lt;code&gt;tsl::robin_map&lt;/code&gt;. You can skip the migration with a &lt;code&gt;FrameConfig::initial_reserve&lt;/code&gt; sizing hint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed 36-byte header + isolated binary attachments.&lt;/strong&gt; A router can read message ID, source, target, and flags without touching the payload. Raw IQ samples or spectrum snapshots travel as attachments via &lt;code&gt;std::move&lt;/code&gt;, not through the key/value store.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest secret: the wire format is MessagePack
&lt;/h2&gt;

&lt;p&gt;Let me get the elephant in the room out of the way: MessageFrame does not invent a wire format. The payload is a flat &lt;strong&gt;MessagePack&lt;/strong&gt; map, and the two-part keys are joined into a single string with the ASCII Unit Separator byte (&lt;code&gt;0x1F&lt;/code&gt;). Any MessagePack-compatible parser can decode the body.&lt;/p&gt;

&lt;p&gt;That decision has one very useful side effect for this benchmark: &lt;strong&gt;msgpack-cxx is the same wire format without the framework.&lt;/strong&gt; So the msgpack column is not a competitor, it is a baseline floor. It shows what the raw format costs when you bring your own value model, your own header, your own key handling, and your own container. MessageFrame is what you pay to not write those yourself.&lt;/p&gt;

&lt;p&gt;This also means the benchmark is the fair version of "MessagePack with a framework layer on top", not a cherry-picked "my format is fastest" exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark: same message, four backends
&lt;/h2&gt;

&lt;p&gt;All four backends receive the identical logical message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;N&lt;/code&gt; parameters with mixed types (&lt;code&gt;double&lt;/code&gt;, &lt;code&gt;int64&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;), cycled deterministically so wire sizes stay constant between runs.&lt;/li&gt;
&lt;li&gt;Keys are two-part (&lt;code&gt;device&lt;/code&gt;, &lt;code&gt;parameter&lt;/code&gt;) in MessageFrame and flat &lt;code&gt;"d0.p0"&lt;/code&gt; strings elsewhere.&lt;/li&gt;
&lt;li&gt;One scenario adds a 1 MiB binary attachment, standing in for an IQ capture.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;full per-message cycle&lt;/strong&gt; is timed: parameter addition -&amp;gt; serialization -&amp;gt; deserialization, &lt;strong&gt;min of 3 passes&lt;/strong&gt;, single thread.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protobuf side uses the closest structural equivalent of MessageFrame's model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;TelemetryValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;oneof&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt; &lt;span class="na"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;TelemetryFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TelemetryValue&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="na"&gt;attachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build: repository Release defaults — &lt;code&gt;-O3 -march=native&lt;/code&gt; plus LTO — on an Intel Core 7 240H (Ubuntu 22.04, GCC).&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: time per message (µs)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;MessageFrame&lt;/th&gt;
&lt;th&gt;MF + hint&lt;/th&gt;
&lt;th&gt;protobuf&lt;/th&gt;
&lt;th&gt;nlohmann/json&lt;/th&gt;
&lt;th&gt;msgpack-cxx*&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 parameters&lt;/td&gt;
&lt;td&gt;0.59&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;td&gt;1.64&lt;/td&gt;
&lt;td&gt;0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 parameters&lt;/td&gt;
&lt;td&gt;3.04&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;4.76&lt;/td&gt;
&lt;td&gt;10.56&lt;/td&gt;
&lt;td&gt;1.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;127 parameters (vector mode)&lt;/td&gt;
&lt;td&gt;11.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;19.52&lt;/td&gt;
&lt;td&gt;47.09&lt;/td&gt;
&lt;td&gt;3.57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150 parameters (hash-map mode)&lt;/td&gt;
&lt;td&gt;24.01&lt;/td&gt;
&lt;td&gt;19.85&lt;/td&gt;
&lt;td&gt;22.76&lt;/td&gt;
&lt;td&gt;64.94&lt;/td&gt;
&lt;td&gt;4.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024 parameters&lt;/td&gt;
&lt;td&gt;196.50&lt;/td&gt;
&lt;td&gt;157.46&lt;/td&gt;
&lt;td&gt;303.52&lt;/td&gt;
&lt;td&gt;471.40&lt;/td&gt;
&lt;td&gt;27.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 params + 1 MiB attachment&lt;/td&gt;
&lt;td&gt;189.76&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;276.07&lt;/td&gt;
&lt;td&gt;14,159&lt;/td&gt;
&lt;td&gt;138.86&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* msgpack-cxx has no in-memory document, header, or two-part keys — it is the wire-format floor, not an equivalent API.&lt;/p&gt;

&lt;p&gt;Where the time goes (add / serialize / deserialize, µs):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;add&lt;/th&gt;
&lt;th&gt;serialize&lt;/th&gt;
&lt;th&gt;deserialize&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 params&lt;/td&gt;
&lt;td&gt;MessageFrame&lt;/td&gt;
&lt;td&gt;0.13&lt;/td&gt;
&lt;td&gt;0.17&lt;/td&gt;
&lt;td&gt;0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 params&lt;/td&gt;
&lt;td&gt;protobuf&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;td&gt;0.16&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 params&lt;/td&gt;
&lt;td&gt;nlohmann/json&lt;/td&gt;
&lt;td&gt;0.22&lt;/td&gt;
&lt;td&gt;0.36&lt;/td&gt;
&lt;td&gt;1.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024 params&lt;/td&gt;
&lt;td&gt;MessageFrame&lt;/td&gt;
&lt;td&gt;82.80&lt;/td&gt;
&lt;td&gt;36.96&lt;/td&gt;
&lt;td&gt;76.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024 params&lt;/td&gt;
&lt;td&gt;MessageFrame + hint&lt;/td&gt;
&lt;td&gt;52.78&lt;/td&gt;
&lt;td&gt;35.03&lt;/td&gt;
&lt;td&gt;70.54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024 params&lt;/td&gt;
&lt;td&gt;protobuf&lt;/td&gt;
&lt;td&gt;94.02&lt;/td&gt;
&lt;td&gt;59.39&lt;/td&gt;
&lt;td&gt;152.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024 params&lt;/td&gt;
&lt;td&gt;nlohmann/json&lt;/td&gt;
&lt;td&gt;145.32&lt;/td&gt;
&lt;td&gt;62.08&lt;/td&gt;
&lt;td&gt;264.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB&lt;/td&gt;
&lt;td&gt;MessageFrame&lt;/td&gt;
&lt;td&gt;25.93&lt;/td&gt;
&lt;td&gt;44.74&lt;/td&gt;
&lt;td&gt;116.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB&lt;/td&gt;
&lt;td&gt;protobuf&lt;/td&gt;
&lt;td&gt;116.12&lt;/td&gt;
&lt;td&gt;113.90&lt;/td&gt;
&lt;td&gt;46.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB&lt;/td&gt;
&lt;td&gt;nlohmann/json&lt;/td&gt;
&lt;td&gt;1,457&lt;/td&gt;
&lt;td&gt;3,725&lt;/td&gt;
&lt;td&gt;8,977&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the wire sizes (bytes):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;MessageFrame&lt;/th&gt;
&lt;th&gt;protobuf&lt;/th&gt;
&lt;th&gt;nlohmann/json&lt;/th&gt;
&lt;th&gt;msgpack-cxx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 params&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;127 params&lt;/td&gt;
&lt;td&gt;1,816&lt;/td&gt;
&lt;td&gt;2,237&lt;/td&gt;
&lt;td&gt;1,986&lt;/td&gt;
&lt;td&gt;1,550&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024 params&lt;/td&gt;
&lt;td&gt;15,297&lt;/td&gt;
&lt;td&gt;18,858&lt;/td&gt;
&lt;td&gt;16,795&lt;/td&gt;
&lt;td&gt;13,237&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB attachment&lt;/td&gt;
&lt;td&gt;1,048,662&lt;/td&gt;
&lt;td&gt;1,048,646&lt;/td&gt;
&lt;td&gt;1,398,188&lt;/td&gt;
&lt;td&gt;1,048,645&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the numbers actually say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MessageFrame beats protobuf by 1.5-2x on large frames.&lt;/strong&gt; At 1,024 parameters the full round-trip is 196 µs vs 303 µs, and the gap is mostly in &lt;code&gt;deserialize&lt;/code&gt; (76 vs 153 µs). At 32 and 127 parameters it wins clearly too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At 4 and 150 parameters the two are statistically tied.&lt;/strong&gt; The 150-parameter row is the honest one to explain: that is exactly where MessageFrame crosses the vector-to-map threshold and pays the one-time migration of 128 entries. The &lt;code&gt;FrameConfig::initial_reserve&lt;/code&gt; hint skips it and drops the round-trip from 24.0 to 19.9 µs. Without the hint it is a tie; with the hint it wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nlohmann/json loses everywhere, and loses badly with binary data.&lt;/strong&gt; 1.64 µs vs 0.59 µs on the smallest frame, and 14.2 &lt;em&gt;milliseconds&lt;/em&gt; vs 190 µs with the 1 MiB attachment — roughly &lt;strong&gt;75x slower&lt;/strong&gt; — because the attachment is base64-encoded twice (in and out). That is the real price of a text format for binary payloads, not a flaw in the library. But the practical consequence matters more: a 14 ms stall per frame is a &lt;strong&gt;blocker for SDR and real-time workloads&lt;/strong&gt;, where the whole point is streaming IQ samples at a fixed cadence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;msgpack-cxx is faster, and that is fine.&lt;/strong&gt; At 1,024 parameters raw MessagePack is 27 µs vs MessageFrame's 196 µs. That gap is the entire framework: the typed value model, the header, the two-part keys, the hybrid container, the zero-allocation path. If you are happy building all of that yourself, use raw msgpack. Most people are not.&lt;/p&gt;

&lt;p&gt;A few fairness notes, because benchmarks without them are marketing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;protobuf&lt;/strong&gt; uses the standard heap API with no &lt;code&gt;Arena&lt;/code&gt;. An &lt;code&gt;Arena&lt;/code&gt; would shave some of its allocation overhead on large messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MessageFrame copies attachments on deserialize&lt;/strong&gt; (116 µs vs protobuf's 46 µs in the 1 MiB row). This is the documented zero-copy trade-off: the library builds a fresh &lt;code&gt;std::vector&lt;/code&gt; instead of aliasing the input buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCC LTO (our repo Release default) slows raw msgpack-cxx's packer by about 2x&lt;/strong&gt; (a codegen quirk we reproduced); MessageFrame, protobuf, and nlohmann/json are unaffected by the setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to choose what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MessageFrame&lt;/strong&gt; — you need runtime-defined messages, dynamic device lineups, no code generation, and a hard C++17 constraint. SDR telemetry, IoT gateways, robotics config dumps, IPC between embedded nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protobuf&lt;/strong&gt; — your schemas are set in stone, you work across many teams and languages, and you want the full ecosystem (validators, reflection, mature tooling). The codegen tax is fine when the schema never changes. Note the allocation story too: MessageFrame gives zero-allocation insertion up to 128 parameters out of the box (flat vector + SSO for keys), while Protobuf only gets there if you write manual &lt;code&gt;google::protobuf::Arena&lt;/code&gt; plumbing yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON&lt;/strong&gt; — you need to debug by eyeballing packets and bandwidth/CPU budget is generous. Not a candidate for real-time binary-heavy traffic: the ~75x slowdown on binary payloads makes it a blocker for SDR and real-time tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Every number in this post is reproducible from the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--recursive&lt;/span&gt; https://github.com/stubcpp/MessageFrame.git
&lt;span class="nb"&gt;cd &lt;/span&gt;MessageFrame
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release &lt;span class="nt"&gt;-DMSGFRAME_BUILD_CROSS_BENCHMARK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;-j&lt;/span&gt;
./build/mf_crossbench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness lives in &lt;code&gt;benchmarks/cross_format&lt;/code&gt; — one shared scenario definition, one backend file per format, and a single &lt;code&gt;main.cpp&lt;/code&gt; running min-of-3 passes. If your numbers differ from mine, that is exactly the point of publishing the harness: it is a conversation starter, not a slogan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;MessageFrame is not a universal replacement for Protobuf, and it is not faster than raw MessagePack. What it does is close the gap between the two: the ergonomics of dynamic messaging — no schemas, no codegen, typed access, zero-allocation hot paths — at roughly a fifth of raw MessagePack's cost and consistently faster than Protobuf and JSON on the workloads that matter for telemetry.&lt;/p&gt;

&lt;p&gt;The full methodology, per-phase timings, and caveats are in &lt;a href="https://github.com/stubcpp/MessageFrame/blob/master/docs/performance.md" rel="noopener noreferrer"&gt;docs/performance.md&lt;/a&gt; in the repo. If you run the harness and get different numbers, or if you want a backend for your favorite format added, open an issue or a PR.&lt;/p&gt;

&lt;p&gt;And if this post made you want to try it — the project is at &lt;a href="https://github.com/stubcpp/MessageFrame" rel="noopener noreferrer"&gt;MessageFrame&lt;/a&gt;. A star helps other developers find it.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>opensource</category>
      <category>networking</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>MessageFrame: Structured C++ Telemetry Without Schema Files — Simple, Header-Only Serialization</title>
      <dc:creator>Serjio</dc:creator>
      <pubDate>Wed, 01 Jul 2026 18:21:09 +0000</pubDate>
      <link>https://dev.to/serjioua/a-small-c-library-for-sending-structured-commands-and-telemetry-between-devices-no-schema-2phf</link>
      <guid>https://dev.to/serjioua/a-small-c-library-for-sending-structured-commands-and-telemetry-between-devices-no-schema-2phf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nlmscdzzpwj8aj8mkqr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nlmscdzzpwj8aj8mkqr.jpg" alt=" " width="591" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever tried to build a simple command/telemetry protocol between a PC and a fleet of SDR receivers, sensors, or embedded devices, you know the usual options aren't great:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Roll your own binary format — fast, but you end up writing and maintaining custom serialization code for every device type, and debugging mismatched structs across machines is painful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Protobuf&lt;/code&gt; / &lt;code&gt;FlatBuffers&lt;/code&gt; — robust, but require you to define your message layout in a schema file upfront, run a code generator as part of your build, and commit to a fixed structure. Adding a new device type or a new parameter means editing the schema, regenerating, recompiling everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JSON over the wire — easy to debug, but heavy for anything real-time or bandwidth-constrained.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran into this while working on a multi-SDR receiver system and ended up writing &lt;a href="https://github.com/stubcpp/MessageFrame" rel="noopener noreferrer"&gt;MessageFrame&lt;/a&gt; — a small C++17 library that lets you build structured messages dynamically, without any schema files or code generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The basic idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of defining a struct for each device type, you address each parameter with two strings — a device name and a parameter name — and the library handles the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One message, multiple devices, assembled at runtime&lt;/span&gt;
&lt;span class="n"&gt;msgframe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MessageFrame&lt;/span&gt; &lt;span class="nf"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MSG_TELEMETRY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TYPE_PERIODIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/*src=*/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/*tgt=*/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sdr_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rx_gain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sdr_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"center_freq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;915'000'000.0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sdr_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sample_rate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2'000'000.0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sdr_2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rx_gain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sdr_2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"lock_status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"psu_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"voltage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;12.04&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"psu_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"temp_c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;47.3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Attach raw IQ data alongside the parameters&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;iq_buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x04&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_attachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"raw_iq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iq_buffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Serialize into a buffer, send over whatever transport you use&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;send_udp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;target_addr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the receiving end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;msgframe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MessageFrame&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sdr_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rx_gain"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;cout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s"&gt;"SDR-1 gain: "&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No generated structs, no schema to maintain. A new device shows up in your network — you just start adding its parameters to the message with its own device key, and the receiver can read them by name immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's under the hood (briefly)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Wire format is &lt;strong&gt;MessagePack&lt;/strong&gt; — compact binary, not JSON, so it's reasonable for real-time use. Any MessagePack-compatible parser on the receiving end can decode it, not just this library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Parameters are stored in a flat &lt;code&gt;std::vector&lt;/code&gt; for small messages (≤128 parameters) — good CPU L1 cache behavior for the common case. Past that the container automatically switches to a &lt;strong&gt;hash map&lt;/strong&gt;. For typical device telemetry (a handful of parameters per device, a handful of devices) you stay in the fast vector path the whole time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;header&lt;/strong&gt; is fixed-size and sits at the front of the packet, so a router or dispatcher can read message ID, source, destination, and timestamp without parsing the full payload.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Binary &lt;strong&gt;attachments&lt;/strong&gt; (IQ samples, spectrum snapshots, raw byte streams) travel alongside parameters in the same packet without going through the key/value store.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it's not&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's not a transport layer — you still bring your own socket/queue/serial port. It's not a replacement for &lt;strong&gt;Protobuf&lt;/strong&gt; if you need cross-language compatibility out of the box or have very tight bandwidth constraints. It's a lightweight way to structure the payload when your message content is dynamic and you don't want to maintain a schema file every time your device lineup changes.&lt;/p&gt;

&lt;p&gt;Repo with examples and benchmarks: &lt;a href="https://github.com/stubcpp/MessageFrame" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>network</category>
      <category>embedded</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
