<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://www.knickish.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.knickish.com/" rel="alternate" type="text/html" /><updated>2024-09-01T19:28:32+00:00</updated><id>http://www.knickish.com/feed.xml</id><title type="html">~</title><entry><title type="html">just the important bits</title><link href="http://www.knickish.com/rust/2024/01/26/just-the-important-bits.html" rel="alternate" type="text/html" title="just the important bits" /><published>2024-01-26T17:00:00+00:00</published><updated>2024-01-26T17:00:00+00:00</updated><id>http://www.knickish.com/rust/2024/01/26/just-the-important-bits</id><content type="html" xml:base="http://www.knickish.com/rust/2024/01/26/just-the-important-bits.html"><![CDATA[<h3 id="what">what</h3>
<p><code class="language-plaintext highlighter-rouge">StructDiff</code> - a zero-dependency, field-level delta compression crate which generates serializable diffs</p>

<h3 id="where">where</h3>
<p><a href="https://github.com/knickish/structdiff" referrer="www.knickish.com">github</a> / <a href="https://crates.io/crates/structdiff">crates</a></p>

<h3 id="why">why</h3>
<p>I’ve been poking at building a multiplayer game for a couple of years now, and one of the big issues that I immediately ran in to was the necessary amount of data transfers. For an MMO-style game, there’s an incredible amount of data that needs to be tracked by the server and also available for the clients in case it’s needed.</p>

<details>
  <summary>things to track for an MMO</summary>

  <div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">-</span> State of Self
<span class="p">    -</span> Health
<span class="p">    -</span> Location
<span class="p">    -</span> Speed
<span class="p">    -</span> Abilities
<span class="p">    -</span> Inventory
<span class="p">    -</span> Chat Messages
<span class="p">    -</span> Reputations
<span class="p">-</span> State of Other (times number of nearby characters)
<span class="p">    -</span> Proxy value for most of the Self stats
<span class="p">-</span> State of World
<span class="p">    -</span> Relevent events
<span class="p">    -</span> Prevailing conditions
</code></pre></div>  </div>

</details>
<p></p>

<p>This is clearly a ton of data, to the point that it will probably be impractical to serialize the entire state and package it each time if you have enough clients connected. This leaves us with two options for communicating changes in state (that I’m aware of):</p>
<ul>
  <li>Events</li>
  <li>Differences</li>
</ul>

<p>Obviously a purely event-driven architecture with deterministic resultion of effects would be the ideal solution here. However, this requires that either a full mirror of the server is run by each client (and each client has all of the necessary information to run this server), or that a completely different set of events be used internally on the server and externally to update client state. Depending on how deterministic your physics engine is, this may or may not be possible. It certainly is a good solution, the 2nd type of system was more appealing to me: one based on differences. (As a sidenote, this has been a huge rabbit trail, I do not recommend writing libraries if you ever hope to actually release a game)</p>

<p>Delta compression at the binary level is an extremely common tactic to reduce the amount of data transfer over the network, but it doesn’t really help with the problem of reducing (de)serialization load, and also discards a large amount of the semantic data that is readily available prior to serialization. The pre-serialization delta-compression approach takes advantage of the fact that it is possible to know which fields have changed and only send those particular updates to a client. There are a couple of excellent struct-diffing libraries that existed already (<a href="https://crates.io/crates/serde-diff">serde-diff</a>, <a href="https://crates.io/crates/diff-struct">DiffStruct</a>), but there was still a niche there (in my opinion, at least) for a library that operated pre-serialization, did not pull <code class="language-plaintext highlighter-rouge">syn</code>/<code class="language-plaintext highlighter-rouge">quote</code>/<code class="language-plaintext highlighter-rouge">proc_macro2</code> into the build tree, and generated serializable diffs. If any of those requirements don’t concern you, one of those libraries should probably be used instead.</p>

<h3 id="how">how</h3>
<p>My first step was to come up with a trait definition I liked. I eventually landed on the following (after an immediate flop of an attempt to RFC <a href="https://github.com/rust-lang/rfcs/pull/3342">anonymous associated types</a>).</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">trait</span> <span class="n">StructDiff</span> <span class="p">{</span>
    <span class="k">type</span> <span class="n">Diff</span><span class="p">:</span> <span class="nb">Clone</span><span class="p">;</span>
    <span class="k">type</span> <span class="n">DiffRef</span><span class="o">&lt;</span><span class="nv">'target</span><span class="o">&gt;</span><span class="p">:</span> <span class="nb">Clone</span> <span class="o">+</span> <span class="nb">Into</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Diff</span><span class="o">&gt;</span>
    <span class="k">where</span>
        <span class="k">Self</span><span class="p">:</span> <span class="nv">'target</span><span class="p">;</span>

    <span class="cd">/// Generate an owned diff between two instances of a struct.</span>
    <span class="k">fn</span> <span class="nf">diff</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">updated</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">Self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Diff</span><span class="o">&gt;</span><span class="p">;</span>

    <span class="cd">/// Generate an unowned diff between two instances of a struct.</span>
    <span class="k">fn</span> <span class="n">diff_ref</span><span class="o">&lt;</span><span class="nv">'target</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="nv">'target</span> <span class="k">self</span><span class="p">,</span> <span class="n">updated</span><span class="p">:</span> <span class="o">&amp;</span><span class="nv">'target</span> <span class="k">Self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">DiffRef</span><span class="o">&lt;</span><span class="nv">'target</span><span class="o">&gt;&gt;</span><span class="p">;</span>


    <span class="cd">/// Apply a single-field diff to a mutable self ref</span>
    <span class="k">fn</span> <span class="nf">apply_single</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">diff</span><span class="p">:</span> <span class="k">Self</span><span class="p">::</span><span class="n">Diff</span><span class="p">);</span>

    <span class="cd">/// Apply a full diff to a mutable self ref</span>
    <span class="k">fn</span> <span class="nf">apply_mut</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">diffs</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Diff</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">diff</span> <span class="k">in</span> <span class="n">diffs</span> <span class="p">{</span>
            <span class="k">self</span><span class="nf">.apply_single</span><span class="p">(</span><span class="n">diff</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="cd">/// ... skipping the by-value and by-ref where Self: Clone impls</span>
<span class="p">}</span>
</code></pre></div></div>

<p>which allows generating a delta to be as simple as:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">structdiff</span><span class="p">::{</span><span class="n">Difference</span><span class="p">,</span> <span class="n">StructDiff</span><span class="p">};</span>
<span class="nd">#[derive(Debug,</span> <span class="nd">PartialEq,</span> <span class="nd">Clone,</span> <span class="nd">Difference)]</span>
<span class="k">struct</span> <span class="n">Example</span> <span class="p">{</span>
    <span class="n">field1</span><span class="p">:</span> <span class="nb">f64</span><span class="p">,</span>
    <span class="n">field2</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">let</span> <span class="n">first</span> <span class="o">=</span> <span class="n">Example</span> <span class="p">{</span>
    <span class="n">field1</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
    <span class="n">field2</span><span class="p">:</span> <span class="s">"Hello"</span><span class="nf">.to_string</span><span class="p">()</span>
<span class="p">};</span>
<span class="k">let</span> <span class="n">second</span> <span class="o">=</span> <span class="n">Example</span> <span class="p">{</span>
    <span class="n">field1</span><span class="p">:</span> <span class="mf">3.14</span><span class="p">,</span>
    <span class="n">field2</span><span class="p">:</span> <span class="s">"Hello"</span><span class="nf">.to_string</span><span class="p">()</span>
<span class="p">};</span>

<span class="k">let</span> <span class="n">diffs</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;&lt;</span><span class="n">Example</span> <span class="k">as</span> <span class="n">StructDiff</span><span class="o">&gt;</span><span class="p">::</span><span class="n">Diff</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">first</span><span class="nf">.diff</span><span class="p">(</span><span class="o">&amp;</span><span class="n">second</span><span class="p">);</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">diffs</span><span class="nf">.len</span><span class="p">(),</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">let</span> <span class="n">diffed</span> <span class="o">=</span> <span class="n">first</span><span class="nf">.apply</span><span class="p">(</span><span class="n">diffs</span><span class="p">);</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">diffed</span><span class="p">,</span> <span class="n">second</span><span class="p">);</span>
</code></pre></div></div>

<p>Notice how the user of the trait never has to interact with the actual type used for the <code class="language-plaintext highlighter-rouge">StructDiff::Diff</code> (or <code class="language-plaintext highlighter-rouge">StructDiff::DiffRef&lt;'_&gt;</code>) associated type. Using the <code class="language-plaintext highlighter-rouge">Diff</code> type <code class="language-plaintext highlighter-rouge">.clone()</code>s its way through the non-matching fields, while the <code class="language-plaintext highlighter-rouge">DiffRef&lt;'_&gt;</code> type attempts to take everything by reference. There’s obviously a ton of boileplate code required with this approach, and I really did not want to have to manually implement this trait for every single struct in my game. Really wish rust had compile-time reflection, but a proc-macro gets it done for now.</p>

<details>
  <summary>big annoying trait impl</summary>
  <p><br />
Deriving diffence on this struct:</p>

  <div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Difference)]</span>
<span class="nd">#[difference(setters)]</span> <span class="c1">// this attribute is explained later in the post</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Test</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">test1</span><span class="p">:</span> <span class="nb">i32</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">test2</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">test3</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">i32</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">test4</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">test5</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">usize</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div>  </div>
  <p>generates the following code</p>
  <div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">_</span><span class="p">:</span> <span class="p">()</span> <span class="o">=</span> <span class="p">{</span>
    <span class="k">use</span> <span class="nn">structdiff</span><span class="p">::</span><span class="nn">collections</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>

    <span class="cd">/// Generated type from StructDiff</span>
    <span class="nd">#[derive(Clone)]</span>
    <span class="k">pub</span> <span class="k">enum</span> <span class="n">__TestStructDiffEnum</span> <span class="p">{</span>
        <span class="nf">test1</span><span class="p">(</span><span class="nb">i32</span><span class="p">),</span>
        <span class="nf">test2</span><span class="p">(</span><span class="nb">String</span><span class="p">),</span>
        <span class="nf">test3</span><span class="p">(</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">i32</span><span class="o">&gt;</span><span class="p">),</span>
        <span class="nf">test4</span><span class="p">(</span><span class="nb">f32</span><span class="p">),</span>
        <span class="nf">test5</span><span class="p">(</span><span class="nb">Option</span><span class="o">&lt;</span><span class="nb">usize</span><span class="o">&gt;</span><span class="p">),</span>
    <span class="p">}</span>

    <span class="cd">/// Generated type from StructDiff</span>
    <span class="nd">#[derive(Clone)]</span>
    <span class="k">pub</span> <span class="k">enum</span> <span class="n">__TestStructDiffEnumRef</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;</span>
    <span class="k">where</span>
        <span class="k">Self</span><span class="p">:</span> <span class="nv">'__diff_target</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="nf">test1</span><span class="p">(</span><span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="nb">i32</span><span class="p">),</span>
        <span class="nf">test2</span><span class="p">(</span><span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="nb">String</span><span class="p">),</span>
        <span class="nf">test3</span><span class="p">(</span><span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">i32</span><span class="o">&gt;</span><span class="p">),</span>
        <span class="nf">test4</span><span class="p">(</span><span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="nb">f32</span><span class="p">),</span>
        <span class="nf">test5</span><span class="p">(</span><span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">usize</span><span class="o">&gt;</span><span class="p">),</span>
    <span class="p">}</span>
    <span class="k">impl</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;</span> <span class="nb">Into</span><span class="o">&lt;</span><span class="n">__TestStructDiffEnum</span><span class="o">&gt;</span> <span class="k">for</span> <span class="n">__TestStructDiffEnumRef</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">fn</span> <span class="nf">into</span><span class="p">(</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">__TestStructDiffEnum</span> <span class="p">{</span>
            <span class="k">match</span> <span class="k">self</span> <span class="p">{</span>
                <span class="nn">__TestStructDiffEnumRef</span><span class="p">::</span><span class="nf">test1</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">__TestStructDiffEnum</span><span class="p">::</span><span class="nf">test1</span><span class="p">(</span><span class="n">v</span><span class="nf">.clone</span><span class="p">()),</span>
                <span class="nn">__TestStructDiffEnumRef</span><span class="p">::</span><span class="nf">test2</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">__TestStructDiffEnum</span><span class="p">::</span><span class="nf">test2</span><span class="p">(</span><span class="n">v</span><span class="nf">.clone</span><span class="p">()),</span>
                <span class="nn">__TestStructDiffEnumRef</span><span class="p">::</span><span class="nf">test3</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">__TestStructDiffEnum</span><span class="p">::</span><span class="nf">test3</span><span class="p">(</span><span class="n">v</span><span class="nf">.clone</span><span class="p">()),</span>
                <span class="nn">__TestStructDiffEnumRef</span><span class="p">::</span><span class="nf">test4</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">__TestStructDiffEnum</span><span class="p">::</span><span class="nf">test4</span><span class="p">(</span><span class="n">v</span><span class="nf">.clone</span><span class="p">()),</span>
                <span class="nn">__TestStructDiffEnumRef</span><span class="p">::</span><span class="nf">test5</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">__TestStructDiffEnum</span><span class="p">::</span><span class="nf">test5</span><span class="p">(</span><span class="n">v</span><span class="nf">.clone</span><span class="p">()),</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">impl</span> <span class="n">StructDiff</span> <span class="k">for</span> <span class="n">Test</span> <span class="p">{</span>
        <span class="k">type</span> <span class="n">Diff</span> <span class="o">=</span> <span class="n">__TestStructDiffEnum</span><span class="p">;</span>
        <span class="k">type</span> <span class="n">DiffRef</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">__TestStructDiffEnumRef</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;</span><span class="p">;</span>
        <span class="k">fn</span> <span class="nf">diff</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">updated</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">Self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Diff</span><span class="o">&gt;</span> <span class="p">{</span>
            <span class="k">let</span> <span class="k">mut</span> <span class="n">diffs</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test1</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test1</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test1</span><span class="p">(</span><span class="n">updated</span><span class="py">.test1</span><span class="nf">.clone</span><span class="p">()))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test2</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test2</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test2</span><span class="p">(</span><span class="n">updated</span><span class="py">.test2</span><span class="nf">.clone</span><span class="p">()))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test3</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test3</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test3</span><span class="p">(</span><span class="n">updated</span><span class="py">.test3</span><span class="nf">.clone</span><span class="p">()))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test4</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test4</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test4</span><span class="p">(</span><span class="n">updated</span><span class="py">.test4</span><span class="nf">.clone</span><span class="p">()))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test5</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test5</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test5</span><span class="p">(</span><span class="n">updated</span><span class="py">.test5</span><span class="nf">.clone</span><span class="p">()))</span>
            <span class="p">};</span>
            <span class="n">diffs</span>
        <span class="p">}</span>
        <span class="k">fn</span> <span class="n">diff_ref</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;</span><span class="p">(</span>
            <span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="k">self</span><span class="p">,</span>
            <span class="n">updated</span><span class="p">:</span> <span class="o">&amp;</span><span class="nv">'__diff_target</span> <span class="k">Self</span><span class="p">,</span>
        <span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">DiffRef</span><span class="o">&lt;</span><span class="nv">'__diff_target</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
            <span class="k">let</span> <span class="k">mut</span> <span class="n">diffs</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test1</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test1</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">DiffRef</span><span class="p">::</span><span class="nf">test1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">updated</span><span class="py">.test1</span><span class="p">))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test2</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test2</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">DiffRef</span><span class="p">::</span><span class="nf">test2</span><span class="p">(</span><span class="o">&amp;</span><span class="n">updated</span><span class="py">.test2</span><span class="p">))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test3</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test3</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">DiffRef</span><span class="p">::</span><span class="nf">test3</span><span class="p">(</span><span class="o">&amp;</span><span class="n">updated</span><span class="py">.test3</span><span class="p">))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test4</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test4</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">DiffRef</span><span class="p">::</span><span class="nf">test4</span><span class="p">(</span><span class="o">&amp;</span><span class="n">updated</span><span class="py">.test4</span><span class="p">))</span>
            <span class="p">};</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.test5</span> <span class="o">!=</span> <span class="n">updated</span><span class="py">.test5</span> <span class="p">{</span>
                <span class="n">diffs</span><span class="nf">.push</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nn">DiffRef</span><span class="p">::</span><span class="nf">test5</span><span class="p">(</span><span class="o">&amp;</span><span class="n">updated</span><span class="py">.test5</span><span class="p">))</span>
            <span class="p">};</span>
            <span class="n">diffs</span>
        <span class="p">}</span>
        <span class="nd">#[inline(always)]</span>
        <span class="k">fn</span> <span class="nf">apply_single</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">diff</span><span class="p">:</span> <span class="k">Self</span><span class="p">::</span><span class="n">Diff</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">match</span> <span class="n">diff</span> <span class="p">{</span>
                <span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test1</span><span class="p">(</span><span class="n">__0</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">self</span><span class="py">.test1</span> <span class="o">=</span> <span class="n">__0</span><span class="p">,</span>
                <span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test2</span><span class="p">(</span><span class="n">__1</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">self</span><span class="py">.test2</span> <span class="o">=</span> <span class="n">__1</span><span class="p">,</span>
                <span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test3</span><span class="p">(</span><span class="n">__2</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">self</span><span class="py">.test3</span> <span class="o">=</span> <span class="n">__2</span><span class="p">,</span>
                <span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test4</span><span class="p">(</span><span class="n">__3</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">self</span><span class="py">.test4</span> <span class="o">=</span> <span class="n">__3</span><span class="p">,</span>
                <span class="k">Self</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test5</span><span class="p">(</span><span class="n">__4</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">self</span><span class="py">.test5</span> <span class="o">=</span> <span class="n">__4</span><span class="p">,</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>
<p></p>

<p>As I said in my requirements, I’ve been attempting to keep <code class="language-plaintext highlighter-rouge">syn</code>/<code class="language-plaintext highlighter-rouge">quote</code>/<code class="language-plaintext highlighter-rouge">proc_macro2</code> out of the build tree of my core gameplay/server library, partly for the compile times and partly because I don’t like monocultures. Nanoserde has been my replacement for the serde ecosystem, and so far it’s actually been very pleasant. My clean build time for my core game library is &lt;6s, and incremental builds are &lt;1s. Nanoserde uses a custom parser as a replacement for syn (which has been <a href="https://twitter.com/knickishd/status/1706367795854823615?s=20">unpopular</a> ), which compiles with its proc-macros in ~1s. It doesn’t parse literally everything in rust, but it’s a significant subset, and I’ve manually implemented the (de)ser traits in the few places where it doesn’t do what I want. When I realized that none of the delta-compression crates fit what I was looking for, lifting the nanoserde parser was an obvious first step (ty open source).</p>

<p>I’d never really written or interacted with a parser at before, so one month-long training montage later, I finally had something that could parse most rust struct definitions (on stable). Most of the heavy lifting in this crate is done in the proc macro (the trait implementation is pretty straightforward), so at this point I just had to figure out how to handle all of the bounds introduced by different features in the crate. Optional bounds that are introduced by different features include <code class="language-plaintext highlighter-rouge">Debug</code>, <code class="language-plaintext highlighter-rouge">serde::Serialize/serde::DeserializeOwned/serde::Deserialize</code>, and <code class="language-plaintext highlighter-rouge">nanoserde::DeBin/nanoserde::SerBin</code>, and each of the generated types needs to be set up appropriately to handle them.</p>

<p>As of the most recent release (v0.6.0, which added the difference-by-reference functionality) I’m finally happy with the API of the crate, and am planning to start diving a bit deeper into performance of diffs between collections, both ordered and unordered. There’s some basic support for diffing collections of hashable objects, but I’m unimpressed with the performance of the methods so far and plan to explore some Levenshtein-distance implementations at some point to try and improve things.</p>

<p>One bright spot on the performance front, however, is the <code class="language-plaintext highlighter-rouge">generated_setters</code> feature of the crate. To use it, you just have to add the <code class="language-plaintext highlighter-rouge">#[difference(setters)]</code> attribute on a struct you derive <code class="language-plaintext highlighter-rouge">Difference</code> on, and a setter will be generated for each field. This setter returns an <code class="language-plaintext highlighter-rouge">Option&lt;&lt;Self as StructDiff&gt;::Diff&gt;</code>, allowing you to modify data structures and record the differences to send simultaneously. Using the same example struct as before, the following code is generated when this feature is enabled:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">impl</span> <span class="n">Test</span> <span class="p">{</span>
    <span class="cd">/// Setter generated by StructDiff. Use to set the test1 field and generate a diff if necessary</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">set_test1_with_diff</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">i32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;&lt;</span><span class="k">Self</span> <span class="k">as</span> <span class="n">StructDiff</span><span class="o">&gt;</span><span class="p">::</span><span class="n">Diff</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">self</span><span class="py">.test1</span> <span class="o">==</span> <span class="n">value</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
        <span class="p">};</span>
        <span class="k">let</span> <span class="n">diff</span> <span class="o">=</span> <span class="o">&lt;</span><span class="k">Self</span> <span class="k">as</span> <span class="n">StructDiff</span><span class="o">&gt;</span><span class="p">::</span><span class="nn">Diff</span><span class="p">::</span><span class="nf">test1</span><span class="p">(</span><span class="n">value</span><span class="nf">.clone</span><span class="p">());</span>
        <span class="k">self</span><span class="py">.test1</span> <span class="o">=</span> <span class="n">value</span><span class="p">;</span>
        <span class="k">return</span> <span class="nf">Some</span><span class="p">(</span><span class="n">diff</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="c1">// ... repeat for other fields</span>
<span class="p">}</span>
</code></pre></div></div>
<p>…which (because <code class="language-plaintext highlighter-rouge">Option</code> is <code class="language-plaintext highlighter-rouge">IntoIterator</code>) means that the following code can be used when working with objects that you know will need to be synced:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="k">mut</span> <span class="n">player</span> <span class="o">=</span> <span class="nn">Test</span><span class="p">::</span><span class="nf">default</span><span class="p">();</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">diffs</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
<span class="n">diffs</span><span class="nf">.extend</span><span class="p">(</span><span class="n">player</span><span class="nf">.set_test1_with_diff</span><span class="p">(</span><span class="mi">1</span><span class="p">));</span>
<span class="nf">assert_eq</span><span class="p">(</span><span class="n">diffs</span><span class="nf">.len</span><span class="p">(),</span> <span class="mi">1</span><span class="p">);</span>
<span class="c1">// diffs now contains the changed field</span>
</code></pre></div></div>
<p>You can also rename the setter using the <code class="language-plaintext highlighter-rouge">#[difference(setter_name = {})]</code> field attribute to avoid collisions. There’s probably room to optimize this more, but honestly at this point it’s fast enough that I haven’t noticed it.</p>

<p>That was a lot more words than I expected, but basically <code class="language-plaintext highlighter-rouge">StructDiff</code> is an attempt (for my specific inclinations) to strike a balance between minimal deps, reasonable performance (both run- and compile-time), and reduced network usage, all with as usable/simple of an API as I can come up with. I wouldn’t go so far as to call it battle-tested, but I’ve been using it in my gamedev project for quite a while now and I am definitely much more limited by my ability to understand linear algebra than I have been by problems with the crate. If you try it out and encounter issues, please let me know! I’m planning to maintain this for the forseeable future, so opening PRs and filing issues is greatly appreciated.</p>]]></content><author><name></name></author><category term="rust" /><summary type="html"><![CDATA[what StructDiff - a zero-dependency, field-level delta compression crate which generates serializable diffs]]></summary></entry><entry><title type="html">hello from heffalump</title><link href="http://www.knickish.com/palm/rust/2023/10/28/hello-from-heffalump.html" rel="alternate" type="text/html" title="hello from heffalump" /><published>2023-10-28T17:00:00+00:00</published><updated>2023-10-28T17:00:00+00:00</updated><id>http://www.knickish.com/palm/rust/2023/10/28/hello-from-heffalump</id><content type="html" xml:base="http://www.knickish.com/palm/rust/2023/10/28/hello-from-heffalump.html"><![CDATA[<p><a href="https://www.knickish.com/palm/rust/2023/09/12/a-mastodon-client-for-palmos.html">previous post on topic</a></p>

<h1 id="-02"><img src="/assets/images/heffalump_logo.png" alt="heffalump logo" /> 0.2</h1>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Reply/Compose</th>
      <th style="text-align: center">Expand</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><img src="/assets/images/heffalump_compose.jpg" alt="heffalump compose" width="87%" /></td>
      <td style="text-align: center"><img src="/assets/images/heffalump_expand.jpg" alt="heffalump expand" width="100%" /></td>
    </tr>
  </tbody>
</table>

<p>New release of heffalump is up now. Lots of improvements including several new features:</p>
<ul>
  <li>Writes:
    <ul>
      <li>Favorites</li>
      <li>Posts</li>
      <li>Reposts</li>
      <li>Replies</li>
    </ul>
  </li>
  <li>An expanded-post view
    <ul>
      <li>Allows viewing the entire text of a long post</li>
    </ul>
  </li>
  <li>HTML parser in conduit
    <ul>
      <li>Handle font/symbol/spacing tags with a best-effort approximation in text</li>
    </ul>
  </li>
</ul>

<p>…and many bugfixes and QoL changes:</p>
<ul>
  <li>Improved text flow and pagination</li>
  <li>Alt-text for media in Reposts and Replies now shown</li>
  <li>Alt-text for non-image media now shown</li>
  <li>Logging and improved error resilience in HotSync Conduit</li>
  <li>Author names now display correctly</li>
  <li>Fixed many cases where text would expand beyond the bottom of the viewable area</li>
</ul>

<p>You can grab <code class="language-plaintext highlighter-rouge">heffalump.prc</code> from the <a href="https://github.com/knickish/heffalump/releases/tag/v0.2.0" referrer="www.knickish.com">v0.2.0 github release</a>, and the conduit from the <a href="https://github.com/knickish/heffalump_conduit" referrer="www.knickish.com">github repo</a>.</p>

<p>Right now Heffalump is working well enough that I’m primarily using my PalmPilot when I check Mastodon. There’s lots of things I’m considering adding at some point, such as <code class="language-plaintext highlighter-rouge">View Pending Actions</code> screen to preview and/or cancel writes before hotsyncing, or fleshing out the <code class="language-plaintext highlighter-rouge">Author</code> handling to allow follows and profile preview, or even a compressed image view in posts which contain media. To both of you actually using Heffalump, feel free to let me know if one of those or something else would improve your experience substantially.</p>]]></content><author><name></name></author><category term="palm" /><category term="rust" /><summary type="html"><![CDATA[previous post on topic]]></summary></entry><entry><title type="html">a mastodon client for palmOS</title><link href="http://www.knickish.com/palm/rust/2023/09/12/a-mastodon-client-for-palmos.html" rel="alternate" type="text/html" title="a mastodon client for palmOS" /><published>2023-09-12T17:00:00+00:00</published><updated>2023-09-12T17:00:00+00:00</updated><id>http://www.knickish.com/palm/rust/2023/09/12/a-mastodon-client-for-palmos</id><content type="html" xml:base="http://www.knickish.com/palm/rust/2023/09/12/a-mastodon-client-for-palmos.html"><![CDATA[<h3 id="recipe-introduction-skip-this">recipe introduction (<a href="#state-of-the-palm">skip this</a>)</h3>
<p>Compared to today’s beautifully packaged and highly sanitized offerings, the pre-iPhone handheld market was the wild west. Huge varieties of form factors were being explored, and the devices which could potentially be replaced by a PDA ranged from MP3 or MiniDisc players, a pad of paper, a GameBoy, <a href="https://palmdb.net/app/noviiremote">an IR TV remote</a>, <a href="https://www.youtube.com/watch?v=b9_Vh9h3Ohw">a cell phone</a>, <a href="https://the-gadgeteer.com/1998/05/03/palm_navigator_review/">a gps unit</a>, or even a <a href="https://www.theverge.com/23801118/imax-movie-palm-pilot-oppenheimer">projector controller</a>. Sadly, the coherent packaging of all of these features in a stylus-free and easy to use device with a centralized app store proved to be the magic bullet Palm was searching for. Apple beat them to the punch, and the rest is history. Working with the palmOS dinosaurs is still enjoyable though; they’re a fascinating window into a past that had a very different idea of the future.</p>

<p>All the palm devices were heavily based around interaction with a home computer. There was limited support for intra-device communication over IR starting with the <a href="https://web.archive.org/web/20081207073255/http://www.palm.com/us/company/pr/1998/palmiii.html">Palm III</a> and an <a href="https://web.archive.org/web/19991128155051/http://palmorder.modusmedia.com/Pu/PU-10300U.htm">add-on</a> which was released in 1998 for the original (1996) Pilot and (1997) PalmPilot, eventually expanding to include Bluetooth and Wi-Fi on the last devices in the mid-to-late aughts. In general, it wasn’t expected that you would be live-tweeting via graffiti. Palm envisioned the handheld as a way to replace your rolodex and communicate asynchronously, largely via email. You would HotSync™ your Palm prior to leaving home, then respond to your emails throughout the day as you had time, before finally sending all your responses at the end of the day when you had access to your home computer (obviously the loop was shorter if you used your work computer). While this relaxed pace of response was quickly outcompeted by Treo/Blackberry-enabled always-on communication, the inherent asynchrony of the early palmOS devices has a certain appeal.</p>

<p>With that in mind, I started trying to find ways to use my PalmPilot instead of my phone where I could. The pen-and-paper replacement functionality still works just fine, obviously; to-do lists tend to not be aided that much by an internet connection. The .mobi standard for ebooks, <a href="https://wiki.mobileread.com/wiki/MOBI#Description">originally based on the palm database format</a>, is declining in use now as epubs become more common, but it’s still pretty easy to find or convert ebooks into that format. The calendar functionality works just fine for a few more years also (until <a href="https://palmdb.net/about/palm-day">Palm Day</a>). The largest challenge nowadays is finding ways to actually communicate (using that desirable async workflow) to anybody who’s not still living in 2001.</p>

<h3 id="state-of-the-palm">state of the palm</h3>
<p>It’s pretty hard nowadays to find good connected apps to use on Palm devices. Unless you’re still running a Windows XP-era version of Outlook and tracking your calendar events in Lotus Notes, your favorite applications probably won’t have support for offline browsing, and modern javascript is an ambitious ask for the venerable Motorola 68K. The ubiquity of HTTPS-by-default is another challenge for a device from a time where hiding your files consisted of setting a bit on its header (and asking all other applications to kindly respect that). With that in mind, I decided to try and come up with something myself, both to practice my C and because I wanted to make stuff like this when I was a kid.</p>

<p>PalmOS application development for interaction with modern web APIs is difficult, but the alternative (writing conduits) has also become unexpectedly challenging, largely because the Conduit Development Kit (CDK) had seemingly bit-rotted off the internet. Luckily for our anxious readers, Dmitry (<a href="https://dmitry.gr/?r=05.Projects&amp;proj=27.%20rePalm">the patron saint of palmOS</a>) had a copy laying around, and it’s now publicly <a href="https://palmdb.net/app/palm-conduit-development-kit-cdk-403-installer">available again courtesy of PalmDB</a>.</p>

<h3 id="make-install-">make install <img src="/assets/images/heffalump_logo.png" alt="heffalump logo" /></h3>
<p>Since I mostly use a PalmPilot Professional, I decided to go the route of using a HotSync conduit for pushing updates to the device, both because Windows tends to have decent backwards compatibility and because I haven’t actually had phone service usable with an on-device modem for about a decade. I’ve been using mastodon instead of twitter as much as possible lately, so I figured an offline client (beginning with just a reader) would be a good place to start. I mostly just read things anyway, so a timeline-download conduit and a timeline-display handheld application seemed like an edible elephant. Also <a href="https://www.c99.org/2023/06/19/palm-viix/">this project</a> I had just read about was pretty cool, so I had the concept on the brain already. With a specific goal in mind finally, I started looking for a toolchain to use.</p>

<p>As a rust programmer by day, I had also largely hallucinated away the difficulties of working with old C toolchains. The choices I identified for developing the handheld application were:</p>

<ol>
  <li>The <a href="https://palmdb.net/app/codewarrior">CodeWarrior</a> IDE and its associated toolchain (2004 - must use 2004 IDE) (It was at this point, dear reader, that I discovered there is a character limit on the palmOS memo app and had to switch)</li>
  <li>A very old version of <a href="https://pilrc.sourceforge.net/">PilRC</a> (2000s - the gnu toolchain from the time)</li>
  <li>An <a href="https://palm2000.com/projects/compilingAndBuildingPalmOsAppsOnUbuntu2004LTS.php">updated toolchain</a> assembled by Dmitry and the owner of the palm2000 website (gcc 9 - no support for statics or jumps &gt; 32k)</li>
</ol>

<p>Option 3 was the obvious choice, but I was still struggling to get everything working. With my toolchain-related limitations in mind, I threw everything in a docker container so I could <em>never have to think about it again</em>™. It’s available <a href="https://github.com/knickish/palm_docker" referrer="www.knickish.com">here</a> if you want to follow along at home. Writing an app for palmOS is <a href="https://archive.org/details/eu_MacTech-1998-03_OCR/page/n50/mode/1up">very different</a> to writing an app for a modern handheld. There is no real attempt to limit your access to anything on the device, and your ability to crash the handheld is limited only by your willingness to write to a NULL pointer or some other application’s data. At this point I pretty much just typed in the hello_world example from the PalmOS Programming Bible, and acquired some bits and bobs from Tavisco’s excellent <a href="https://github.com/Tavisco/Palmkedex">Palmkdex</a> to get things started. The latest build of the client (which I’ve named heffalump, in keeping with mastodon’s pachyderm theme) is available <a href="https://github.com/knickish/heffalump/actions/runs/6166010300" referrer="www.knickish.com">here</a>. It was unreasonably satisfying to build a palmOS app in a containerized GitHub action for some reason. I’m exploring it with my therapist.</p>

<p>Now that an app to display data is made, it’s time to actually load some data. So far as I’m aware, HotSync Manager never had a 64 bit build, and each conduit registered for a application is expected to have an associated 32-bit dll which hotsync will call into. Registering a conduit consists of 1: Writing the appropriate data to a registry key (which requires sequential numbering among the keys for all conduits) and 2: not messing up the registry. Luckily, the CDK has functionality that will do this step for you. Unluckily, I prefer to write rust, so I started with writing a conduit-agnostic wrapper for the Conduit Manager DLL. At this point, I could <em>register</em> a conduit, but had no way to actually make the conduit. Again, I didn’t feel like writing C++97, so I also wrapped the sync logic and made a schmancy builder-pattern installer creator thing for conduits that will definitely be used a whole lot by both people who still use palms. My time management sucks.</p>

<p><img src="/assets/images/heffalump_conduit_install.png" alt="heffalump conduit installed" width="80%" /></p>

<p>At this point I was getting annoyed that I had spent so long on these things, so I just imported <a href="https://github.com/h3poteto/megalodon-rs">megalodon-rs</a> to download my mastodon timeline instead of writing the code myself. The conduit itself is exported as a 32-bit dll with a single entry point called OpenConduit, which HotSync calls after loading your dll. I think there are supposed to be more functions exported, but it works fine so far <code class="language-plaintext highlighter-rouge">¯\_(ツ)_/¯</code>. Internally, the conduit just takes an empty PalmDOC database (PDB) file, downloads the timeline data, then stuffs everything into the PDB and sends the entire thing to the handheld. I doubt any custom HotSync conduit has had an entire tokio runtime stuffed in it before, but it only took me an afternoon to write and it takes ~5s to run, so chalking this one up as a win. You can clone the <a href="https://github.com/knickish/heffalump_conduit" referrer="www.knickish.com">repo here</a>, and install the conduit yourself using the provided binary if you too would like to use the world’s most exclusive mastodon client.</p>

<p><img src="/assets/images/heffalump_final.jpg" alt="heffalump reader" width="50%" /></p>

<p>I learned a ton through this process, and I couldn’t have done it without lots of help from Dmitry, Tavisco, and the other holdouts who still write code and host content for palmOS, so thanks again to them. You can follow me on mastodon <a href="https://hachyderm.io/@knickish">@knickish@hachyderm.io</a>, or join the <a href="https://discord.gg/YddKPpR">PalmDB discord server</a> if you found this content interesting.</p>]]></content><author><name></name></author><category term="palm" /><category term="rust" /><summary type="html"><![CDATA[recipe introduction (skip this) Compared to today’s beautifully packaged and highly sanitized offerings, the pre-iPhone handheld market was the wild west. Huge varieties of form factors were being explored, and the devices which could potentially be replaced by a PDA ranged from MP3 or MiniDisc players, a pad of paper, a GameBoy, an IR TV remote, a cell phone, a gps unit, or even a projector controller. Sadly, the coherent packaging of all of these features in a stylus-free and easy to use device with a centralized app store proved to be the magic bullet Palm was searching for. Apple beat them to the punch, and the rest is history. Working with the palmOS dinosaurs is still enjoyable though; they’re a fascinating window into a past that had a very different idea of the future.]]></summary></entry></feed>