<?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: Pravin Baviskar</title>
    <description>The latest articles on DEV Community by Pravin Baviskar (@pravin_baviskar_d6d9a355e).</description>
    <link>https://dev.to/pravin_baviskar_d6d9a355e</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%2F4070849%2F40643e6d-9b04-4c44-bd22-586a7625d54f.png</url>
      <title>DEV Community: Pravin Baviskar</title>
      <link>https://dev.to/pravin_baviskar_d6d9a355e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pravin_baviskar_d6d9a355e"/>
    <language>en</language>
    <item>
      <title>C to Rust Port</title>
      <dc:creator>Pravin Baviskar</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:32:48 +0000</pubDate>
      <link>https://dev.to/pravin_baviskar_d6d9a355e/c-to-rust-port-13b0</link>
      <guid>https://dev.to/pravin_baviskar_d6d9a355e/c-to-rust-port-13b0</guid>
      <description>&lt;h1&gt;
  
  
  Porting QOI from C to Rust: proving the port actually holds up
&lt;/h1&gt;

&lt;p&gt;For Port Mortem I picked a small target: &lt;code&gt;quite-ok-image-format&lt;/code&gt;, a ~280-line&lt;br&gt;
C99 reference implementation of QOI (Quite OK Image Format), a lossless&lt;br&gt;
image codec built to be dramatically faster than PNG. Small codebase,&lt;br&gt;
clear input/output contract, no external dependencies. Should be a clean&lt;br&gt;
weekend port.&lt;/p&gt;

&lt;p&gt;It wasn't clean. It was much more interesting than clean.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I picked, and why
&lt;/h2&gt;

&lt;p&gt;QOI's whole appeal is simplicity — read raw pixels, emit one of four op&lt;br&gt;
codes per pixel (a cached-color index, a small delta, a bigger delta, or a&lt;br&gt;
raw literal), repeat. That simplicity is exactly what makes it a good&lt;br&gt;
&lt;em&gt;test&lt;/em&gt; of whether a port is honest: there's nowhere to hide. Either your&lt;br&gt;
Rust port emits the identical byte for the identical pixel, or it doesn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I proved behavioral equivalence
&lt;/h2&gt;

&lt;p&gt;Compiling was the easy part (once I patched a Windows-only header issue&lt;br&gt;
and a genuine C syntax bug in the original — more on bugs below). The real&lt;br&gt;
work was proving the Rust port didn't just &lt;em&gt;look&lt;/em&gt; right.&lt;/p&gt;

&lt;p&gt;I built a differential test harness that runs both binaries — C and Rust —&lt;br&gt;
against the same input and diffs their output byte-for-byte, for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encode: does &lt;code&gt;.qok&lt;/code&gt; output match exactly?&lt;/li&gt;
&lt;li&gt;Decode: does round-tripping back to &lt;code&gt;.ppm&lt;/code&gt; match exactly?&lt;/li&gt;
&lt;li&gt;Cross-decode: does C decoding &lt;em&gt;Rust's&lt;/em&gt; output match Rust decoding
&lt;em&gt;its own&lt;/em&gt; output, and vice versa?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran this across nine deliberately different images — solid color runs,&lt;br&gt;
gradients, random noise, a hand-engineered hash collision (two different&lt;br&gt;
colors forced to hash to the same internal cache slot), and a 130-pixel&lt;br&gt;
run that needs to split across three separate run-length chunks. Then I&lt;br&gt;
added a randomized differential fuzzer generating fresh images every&lt;br&gt;
iteration, checking the two implementations agree with each other, run&lt;br&gt;
continuously for 60+ seconds.&lt;/p&gt;

&lt;p&gt;Result: byte-for-byte identical encode output, on every image, every time.&lt;/p&gt;
&lt;h2&gt;
  
  
  The edge case that ate six hours
&lt;/h2&gt;

&lt;p&gt;Everything passed on small hand-written test images. Then I threw a&lt;br&gt;
64x64 image at it — bigger, more varied — and the C and Rust outputs&lt;br&gt;
diverged. No crash. No error. Just different bytes, starting a little&lt;br&gt;
way into the file.&lt;/p&gt;

&lt;p&gt;I spent a long stretch assuming I'd mis-ported some arithmetic — re-deriving&lt;br&gt;
the diff/luma op math by hand, checking bit-packing order, convinced I'd&lt;br&gt;
gotten a sign wrong somewhere. Nothing lined up.&lt;/p&gt;

&lt;p&gt;The actual bug was upstream of all of it, in a single line I'd barely&lt;br&gt;
looked at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;fscanf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"P6 %d %d 255&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;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That trailing &lt;code&gt;\n&lt;/code&gt; in a scanf format string doesn't mean "match one&lt;br&gt;
newline." Per the C standard, it means "consume &lt;em&gt;any and all&lt;/em&gt; following&lt;br&gt;
whitespace." My 64x64 test image happened to start with a block of pixels&lt;br&gt;
valued &lt;code&gt;(10, 10, 10)&lt;/code&gt; — and 10 is the same byte as ASCII newline. &lt;code&gt;fscanf&lt;/code&gt;&lt;br&gt;
was silently eating real pixel data, thinking it was whitespace, and&lt;br&gt;
shifting every subsequent read out of alignment.&lt;/p&gt;

&lt;p&gt;I only found it by writing a tiny standalone C program — not touching the&lt;br&gt;
actual reference file — that printed exactly what the header parser&lt;br&gt;
consumed. First five bytes it read after the header: not what I expected&lt;br&gt;
at all. That's when it clicked.&lt;/p&gt;

&lt;p&gt;The fix wasn't to "correct" the C reference. It was to make the Rust port&lt;br&gt;
consume whitespace the &lt;em&gt;same greedy way&lt;/em&gt; &lt;code&gt;fscanf&lt;/code&gt; does, bug and all —&lt;br&gt;
because the goal was proving equivalence with what the original actually&lt;br&gt;
does, not shipping a quietly-improved version that no longer matches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bugs found along the way
&lt;/h2&gt;

&lt;p&gt;Five in total, all documented with rationale in &lt;code&gt;DECISIONS.md&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;byteswap.h&lt;/code&gt;&lt;/strong&gt; — Linux-only header, doesn't exist on Windows.
Build-environment fix, not a behavioral one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid C&lt;/strong&gt;: a &lt;code&gt;switch&lt;/code&gt; &lt;code&gt;default:&lt;/code&gt; label directly followed by a
variable declaration — not legal C. Needed a brace to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent data loss&lt;/strong&gt;: the encoder drops the final pixel of an image
under certain conditions. Real bug. Left unfixed in the reference,
deliberately reproduced in the Rust port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows text-mode corruption&lt;/strong&gt;: &lt;code&gt;fopen(f, "w")&lt;/code&gt; on Windows silently
rewrites any pixel byte valued 10 into a two-byte CRLF sequence,
corrupting binary image data. Fixed — this one's a portability bug,
not intended behavior worth preserving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;fscanf&lt;/code&gt; whitespace quirk&lt;/strong&gt; described above. Reproduced, not
fixed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The decision I'd take back
&lt;/h2&gt;

&lt;p&gt;I spent too long debugging bug #5 by staring at the Rust and C source side&lt;br&gt;
by side, convinced the answer was in the arithmetic, before writing a&lt;br&gt;
proper isolated diagnostic tool. The diagnostic took fifteen minutes to&lt;br&gt;
write and found the bug in one run. I probably burned three or four hours&lt;br&gt;
before reaching for it. Next time: the instant a bug survives one round of&lt;br&gt;
"read the code harder," stop reading and start printing actual runtime&lt;br&gt;
values. Isolate before you theorize.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "porting" actually means here
&lt;/h2&gt;

&lt;p&gt;The biggest mindset shift for me: a port isn't done when it compiles, and&lt;br&gt;
it isn't even done when it "looks equivalent" on a couple of test images.&lt;br&gt;
It's done when you've actively tried to make it disagree with the&lt;br&gt;
original and failed — repeatedly, on inputs you didn't hand-pick to be&lt;br&gt;
easy. Bugs #3 and #5 could have been quietly "fixed" in the Rust version&lt;br&gt;
and nobody reviewing a diff would have noticed anything wrong. That's&lt;br&gt;
exactly the kind of silent, confident-looking divergence Port Mortem is&lt;br&gt;
designed to catch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repo: &lt;a href="https://github.com/Pravin881/quite-ok-image-format" rel="noopener noreferrer"&gt;https://github.com/Pravin881/quite-ok-image-format&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Built for Port Mortem, Hackathon Raptors — Track A (C to Rust).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hackathonraptors</category>
      <category>portmortem</category>
      <category>rust</category>
      <category>c</category>
    </item>
  </channel>
</rss>
