<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>naftpink</title>
    <link>https://naft.pink</link>
    <description>naftpink — building a server hosting platform, a private diary app, and a game with its own scripting VM.</description>
    <language>en</language>
    <atom:link href="https://naft.pink/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>the only honest test of an obfuscator is writing the deobfuscator</title>
      <link>https://naft.pink/blog/obfuscator-and-deobfuscator</link>
      <guid isPermaLink="true">https://naft.pink/blog/obfuscator-and-deobfuscator</guid>
      <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
      <description>I built a Java obfuscator and the tool that undoes it, both on ASM. Writing the second one is what told me which parts of the first were protection and which were just mess.</description>
      <content:encoded><![CDATA[

<p>Years ago I wrote an <a href="https://naft.pink/projects/java-obfuscator">obfuscator for Java programs and a deobfuscator that
undoes it</a>, both built on ASM, both working on
compiled bytecode rather than source. This was 2021/2022 and I&#x27;m writing about
it from memory rather than from the code, so treat the specifics below as
roughly-remembered rather than a transcript — the lesson is the part I&#x27;m sure
of.</p>
<p>Building the second one is the only reason I learned anything from the first.</p>
<h2 id="an-obfuscator-always-looks-like-it-works">an obfuscator always looks like it works</h2>
<p>This is the trap, and it&#x27;s structural rather than a mistake anyone makes out of
carelessness.</p>
<p>You run your tool. You open the output in a decompiler. It&#x27;s unreadable — names
are gone, the control flow is soup, the strings aren&#x27;t there. It looks like
exactly what you set out to build, and every instinct says you&#x27;re done.</p>
<p>But &quot;I can&#x27;t read this&quot; and &quot;this is expensive to reverse&quot; are different claims,
and only the second one matters. The first is guaranteed by construction: you
scrambled it, so of course it looks scrambled to you. You&#x27;re the least qualified
person to judge, because you&#x27;re reading output you already know the shape of and
grading it on how alien it feels.</p>
<p>The gap between those two claims only becomes visible when somebody sits down
and actually tries to undo it. The fastest way to arrange that is to be them.</p>
<h2 id="bytecode-not-source">bytecode, not source</h2>
<p>Worth saying what layer this operates at, because it changes what&#x27;s available.</p>
<p>The tool doesn&#x27;t rewrite Java and recompile. It reads the <code>.class</code> files
<code>javac</code> already produced and rewrites those directly. ASM is a visitor over the
class format — you hand it a reader, it walks the structure, and you intercept
the parts you care about:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="java" data-theme="rose-pine-moon"><code data-language="java" data-theme="rose-pine-moon"><span data-line=""><span style="color:#3E8FB0">class</span><span style="color:#9CCFD8"> Renamer</span><span style="color:#3E8FB0"> extends</span><span style="color:#C4A7E7;font-style:italic"> ClassVisitor</span><span style="color:#908CAA"> {</span></span>
<span data-line=""><span style="color:#908CAA">    @</span><span style="color:#3E8FB0">Override</span></span>
<span data-line=""><span style="color:#3E8FB0">    public</span><span style="color:#3E8FB0"> MethodVisitor</span><span style="color:#EA9A97"> visitMethod</span><span style="color:#908CAA">(</span></span>
<span data-line=""><span style="color:#3E8FB0">            int</span><span style="color:#C4A7E7;font-style:italic"> access</span><span style="color:#908CAA">,</span><span style="color:#3E8FB0"> String</span><span style="color:#C4A7E7;font-style:italic"> name</span><span style="color:#908CAA">,</span><span style="color:#3E8FB0"> String</span><span style="color:#C4A7E7;font-style:italic"> desc</span><span style="color:#908CAA">,</span><span style="color:#3E8FB0"> String</span><span style="color:#C4A7E7;font-style:italic"> sig</span><span style="color:#908CAA">,</span><span style="color:#3E8FB0"> String</span><span style="color:#908CAA">[]</span><span style="color:#C4A7E7;font-style:italic"> ex</span><span style="color:#908CAA">)</span><span style="color:#908CAA"> {</span></span>
<span data-line=""><span style="color:#3E8FB0">        return</span><span style="color:#E0DEF4;font-style:italic"> super</span><span style="color:#908CAA">.</span><span style="color:#EA9A97">visitMethod</span><span style="color:#908CAA">(</span><span style="color:#E0DEF4">access</span><span style="color:#908CAA">,</span><span style="color:#EA9A97"> rename</span><span style="color:#908CAA">(</span><span style="color:#E0DEF4">name</span><span style="color:#908CAA">),</span><span style="color:#E0DEF4"> desc</span><span style="color:#908CAA">,</span><span style="color:#E0DEF4"> sig</span><span style="color:#908CAA">,</span><span style="color:#E0DEF4"> ex</span><span style="color:#908CAA">);</span></span>
<span data-line=""><span style="color:#908CAA">    }</span></span>
<span data-line=""><span style="color:#908CAA">}</span></span></code></pre></figure>
<p>Two things follow from working down here. The first is that the tool never needs
source, which is the entire premise of the deobfuscator — if you had the source
you wouldn&#x27;t be running it. The second is that the JVM will happily execute
class files <code>javac</code> would never emit. There are shapes down there that no Java
file can describe, and a source-level tool simply cannot reach them.</p>
<h2 id="what-the-reverser-kept-killing">what the reverser kept killing</h2>
<p>The pattern that repeated: I&#x27;d add a transformation, admire how bad the output
looked, then write the undo for it and find out it was a lot less work than the
obfuscation had been.</p>
<p>That&#x27;s the useful signal. A transformation that a mechanical pass reverses
completely isn&#x27;t protection — it&#x27;s <em>noise</em>. It costs you real things: bigger
artefacts, slower loading, sometimes a genuine behaviour change you don&#x27;t notice
for weeks. And it buys nothing against anyone with the patience to write the
reversal.</p>
<p>Renaming is the clearest case. Stripping every identifier down to <code>a</code>, <code>b</code>, <code>aa</code>
feels devastating and mostly isn&#x27;t, because names were never load-bearing — the
structure survives untouched, and a reverser doesn&#x27;t need your names, only
consistent ones they can assign themselves. It raises the cost of <em>reading</em>
without raising the cost of <em>understanding</em>, and the second is the one an
attacker is paying.</p>
<p>What survived my own reversing attempts were the transformations where undoing
them meant reasoning about what the program actually does, not just pattern
matching on what it looks like. Anything a visitor could spot and rewrite
mechanically, a visitor did.</p>
<h2 id="the-part-that-generalises">the part that generalises</h2>
<p>I stopped working on the JVM a long time ago and this is still the project I
think about most, because the lesson wasn&#x27;t about bytecode at all:</p>
<p><strong>You learn almost nothing about a defence by admiring it. You learn everything
by attacking it.</strong></p>
<p>Anyone can build the half that looks strong. The half that tells you the truth
is the one that tries to break it, and it&#x27;s the half people skip, because it&#x27;s
less fun and it mostly produces bad news about work you already did.</p>
<p>That instinct is the thing that carried forward. It&#x27;s why I care about the race
in an <a href="https://naft.pink/projects/art-gallery">auction system</a> where two bids land in the same
instant, and about the <a href="https://naft.pink/projects/dog-grooming">booking slot</a> two people click
at once — not because those failures showed up in testing, but because the
interesting case is the adversarial one and you only find it by going looking on
purpose.</p>
<p>Nobody reports the double-booking bug. They just don&#x27;t come back.</p>
<hr/>
<p>The concrete version of all this — what happened when the deobfuscator was
pointed at <a href="https://naft.pink/blog/string-encryption-ships-the-key">ProGuard and Allatori</a>, and
why encrypted strings hand you the key along with the lock — is a separate note.</p>]]></content:encoded>
      <category>java</category>
      <category>jvm</category>
      <category>bytecode</category>
    </item>
    <item>
      <title>in an encrypted app, the wire schema is the threat model</title>
      <link>https://naft.pink/blog/schema-is-the-threat-model</link>
      <guid isPermaLink="true">https://naft.pink/blog/schema-is-the-threat-model</guid>
      <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
      <description>I&apos;m rewriting my chat app instead of resuming it. The reason isn&apos;t the encryption — it&apos;s that there was never a written-down shape for a message, and in an end-to-end encrypted system that&apos;s not untidiness, it&apos;s an unauditable security boundary.</description>
      <content:encoded><![CDATA[

<p>I&#x27;m restarting my <a href="https://naft.pink/projects/encrypted-chat">self-hosted chat app</a> in 2026
rather than picking it up where it stopped. It ran through 2025, worked, and
never went public.</p>
<p>The interesting part isn&#x27;t that I&#x27;m rewriting it. It&#x27;s that the thing forcing
the rewrite isn&#x27;t the encryption, the realtime layer, or the mobile client. It&#x27;s
that a message never had a written-down shape.</p>
<h2 id="the-tool-that-was-actually-a-bug-report">the tool that was actually a bug report</h2>
<p>At some point I built a logger for my own WebSocket traffic, because I couldn&#x27;t
tell what was going over the wire. This is the line that matters:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="rose-pine-moon"><code data-language="ts" data-theme="rose-pine-moon"><span data-line=""><span style="color:#EA9A97">type</span><span style="color:#908CAA">:</span><span style="color:#E0DEF4;font-style:italic"> data</span><span style="color:#3E8FB0">.</span><span style="color:#E0DEF4;font-style:italic">type</span><span style="color:#3E8FB0"> ??</span><span style="color:#F6C177"> &quot;unknown&quot;</span><span style="color:#908CAA">,</span></span></code></pre></figure>
<p>I wrote that, shipped it, used it for months, and read it as tooling. It isn&#x27;t.
It&#x27;s a defect report about the system it&#x27;s pointed at.</p>
<p>That <code>??</code> exists because the logger cannot know which shapes are legal. It&#x27;s
reaching for a field that might not be there, on an object it parsed out of a
string that might not have been JSON. Every hedge in that expression is
describing something the protocol declined to guarantee.</p>
<p>Here&#x27;s the test I wish I&#x27;d applied: <strong>if you need to build an instrument to
discover the shape of your own messages, the shape doesn&#x27;t exist.</strong> Not &quot;isn&#x27;t
documented&quot; — doesn&#x27;t exist. There was no artefact anywhere in that codebase
that could answer &quot;what is a legal message&quot;, because the answer lived
distributed across every handler that had ever been written, in the heads of
nobody but me, and only for as long as I remembered.</p>
<p>Building the instrument felt like rigour. It was avoidance with a nice console
prefix.</p>
<h2 id="why-this-is-worse-than-untidy-when-its-encrypted">why this is worse than untidy when it&#x27;s encrypted</h2>
<p>If it were an ordinary chat app, no schema is a maintenance problem. Handlers
drift, a field gets renamed in three places and not the fourth, you fix bugs for
a while. Unpleasant, survivable, fixable incrementally.</p>
<p>End-to-end encryption changes the category, because every message is now a
negotiation about what the server is allowed to know.</p>
<p>Some of a message has to be readable by the server — you cannot route what you
cannot read. The recipient, the channel, roughly when: that&#x27;s metadata, and it&#x27;s
load-bearing infrastructure. The rest is ciphertext the server must never be
able to open. The line between those two sets is not an implementation detail.
<strong>It is the security model.</strong> It&#x27;s the entire answer to &quot;what does someone who
seizes this box learn?&quot;</p>
<p>Now: where was that line written down in my system?</p>
<p>Nowhere. It was wherever a given handler happened to put a field. Adding a
feature meant adding a property to an object, and nothing anywhere would tell me
whether I&#x27;d just quietly moved something from the encrypted side to the plaintext
side. No type error. No failing test. No review comment, because it was only me.
The system would keep working perfectly while its security properties changed
underneath it.</p>
<p>You cannot audit a boundary that was never drawn. That&#x27;s the sentence the whole
project turns on, and I didn&#x27;t have it at the time.</p>
<p><a href="https://naft.pink/projects/yunoa">Yunoa</a> dodges this by being single-user — one reader, one
writer, same person, so almost everything can be ciphertext and the question
barely comes up. Put a second participant in the room and the question is the
entire design.</p>
<h2 id="this-is-also-why-its-a-rewrite">this is also why it&#x27;s a rewrite</h2>
<p>The usual advice is that rewrites are a trap, and it&#x27;s usually right. I&#x27;d defend
this one on a narrow ground: <strong>a schema is not a layer you can add later.</strong></p>
<p>Most things are addable. You can bolt on logging, caching, rate limiting, even
authentication, and the existing code mostly doesn&#x27;t need to know. A schema
isn&#x27;t like that, because every handler, every stored message and every
encryption decision was already written against its absence. Introducing one
means touching all of them simultaneously, and there is no intermediate state
where half the system has a contract and the other half doesn&#x27;t — that state is
strictly worse than either end, because now you have a guarantee that&#x27;s only
sometimes true, which is the same as no guarantee plus false confidence.</p>
<p>&quot;Retrofit the schema&quot; and &quot;rewrite it&quot; describe the same amount of work. One of
them is honest about it.</p>
<h2 id="what-im-doing-differently">what I&#x27;m doing differently</h2>
<p>Schema first, before any transport code exists. Every message type declared in
one place, with the encrypted fields and the routable fields separated <em>in the
declaration itself</em>, so that moving something across that line is a visible
change to a file whose only job is to describe the boundary.</p>
<p>Then the protocol on top of the socket that I skipped last time — message IDs,
acknowledgements, an explicit reconnect handshake — instead of calling <code>send</code>
and hoping. And a client that cannot send a message the schema doesn&#x27;t describe,
which is the property that makes all of the above stay true rather than being
true on the day I wrote it.</p>
<h2 id="still-unsolved">still unsolved</h2>
<ul>
<li>Multi-device history. A new device has to read messages it wasn&#x27;t present for, and every clean answer to that is some flavour of &quot;trust something new&quot;.</li>
<li>Group membership changes. What someone who joins today can read of yesterday, and what someone removed can still read tomorrow.</li>
<li>Notifications that are useful without leaking who a message is from.</li>
</ul>
<p>None of these are new problems and none of them are things I&#x27;ll solve better
than the people who already have. The difference is that this time I&#x27;ll be
answering them against a schema that says what a message is — rather than
discovering the answers I&#x27;d accidentally already committed to by reading my own
logs.</p>]]></content:encoded>
      <category>websockets</category>
      <category>encryption</category>
      <category>architecture</category>
    </item>
    <item>
      <title>string encryption ships you the key</title>
      <link>https://naft.pink/blog/string-encryption-ships-the-key</link>
      <guid isPermaLink="true">https://naft.pink/blog/string-encryption-ships-the-key</guid>
      <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
      <description>My Java deobfuscator targeted ProGuard and Allatori jars. This is old enough — 2021/2022 — that I&apos;m writing it from memory, not from the code, and I&apos;ve had to correct details I got backwards the first time I wrote this up.</description>
      <content:encoded><![CDATA[

<p>The <a href="https://naft.pink/projects/java-obfuscator">deobfuscator half</a> of that project had two real
targets: ProGuard and Allatori. Upfront caveat, because it matters more than
usual here: this is a 2021/2022 project, I&#x27;m writing from memory rather than
from the code, and an earlier draft of this post had the two swapped in a way
that didn&#x27;t match what actually happened. What follows is the corrected
version, and I&#x27;d still hold it loosely.</p>
<h2 id="proguard-was-the-more-annoying-one">proguard was the more annoying one</h2>
<p>As best I remember it, ProGuard-obfuscated jars were the side that actually
made me do work. I ran into something like five different methods for deriving
or decrypting a key, and which one a given jar used didn&#x27;t seem consistent from
jar to jar — possibly tied to a version or a build setting, but I never
actually isolated which.</p>
<p>That&#x27;s the honest shape of that half: not one algorithm, a catalogue. You
recognise a pattern, write the reversal, and go looking for the next variant.
I don&#x27;t have a clean account of why there were around five rather than three or
eight — that&#x27;s just how many distinct methods I ran into before I stopped
looking.</p>
<h2 id="allatoris-obstacle-wasnt-the-encryption">allatori&#x27;s obstacle wasn&#x27;t the encryption</h2>
<p>Allatori&#x27;s actual hurdle was different in kind, and simpler than I originally
described it: a deliberately malformed class, built to crash decompilers like
Luyten rather than to resist a targeted tool. It&#x27;s a cheap trick — it does
nothing against something that isn&#x27;t a general-purpose decompiler — but it&#x27;s
effective against exactly the tool most people would reach for first.</p>
<p>Once that class was out of the way, decrypting the strings underneath it wasn&#x27;t
the hard part. Which is worth sitting with, because it&#x27;s the general shape of
this problem regardless of which tool you&#x27;re pointed at:</p>
<blockquote>
<p><strong>You cannot ship someone a program that runs on their machine and also
withhold from them what the program needs in order to run.</strong></p>
</blockquote>
<p>A program that has to decrypt a string to use it has to carry the routine, the
key, and the call site into that decryption, all in the same artefact it hands
you. The string isn&#x27;t protected. It&#x27;s <em>inconvenienced</em> — one indirection away
from plaintext, with the indirection included in the download. That&#x27;s true of
string encryption generally, independent of which specific tool I was pointed
at, and it&#x27;s the part of this whole project I&#x27;m actually confident about.</p>
<h2 id="what-im-not-confident-about-anymore">what I&#x27;m not confident about anymore</h2>
<p>I&#x27;d previously ranked name mangling, string encryption, and control-flow
obfuscation by how much they resisted the deobfuscator, with control flow as
&quot;the one that actually cost me.&quot; I don&#x27;t have a solid memory of testing control
flow obfuscation specifically as part of this project, so I&#x27;m dropping that
ranking rather than keep repeating a claim I can&#x27;t back up.</p>
<p>What I&#x27;ll stand behind: name mangling destroys information rather than hiding
it, so there&#x27;s nothing to reverse, only to relabel. String encryption is
reversible in principle for the reason above. Past that, I&#x27;d rather say &quot;I
don&#x27;t remember clearly&quot; than restate specifics that didn&#x27;t hold up the first
time I wrote them down.</p>
<h2 id="the-part-i-still-use">the part I still use</h2>
<p>I wrote about <a href="https://naft.pink/blog/obfuscator-and-deobfuscator">why building both halves was the point</a>
separately, and the lesson from that post is the one part of this project I&#x27;d
still defend without hedging: you cannot tell which of your own transformations
are strong by admiring the output. You find out by writing the thing that comes
at it from the other side — and, apparently, you should also double-check your
own memory before writing it up years later.</p>]]></content:encoded>
      <category>java</category>
      <category>jvm</category>
      <category>bytecode</category>
    </item>
    <item>
      <title>keroton.in - temporary placeholder that lasted 2 years</title>
      <link>https://naft.pink/blog/kerotonin-cool-but-unused</link>
      <guid isPermaLink="true">https://naft.pink/blog/kerotonin-cool-but-unused</guid>
      <pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate>
      <description>To love is to let go (of temporary) websites</description>
      <content:encoded><![CDATA[

<p>So the story is really simple. One day I wanted to... buy a domain, yeah I know really creative.</p>
<p>So I asked my friend about cool name (he had acod.in at the time)</p>
<p>and after a while &quot;<strong>keroton.in</strong>&quot; was born.
I&#x27;ve made a quick temporary website (placeholder) in 2024... but it stayed that way till 2026.</p>
<h2 id="why-you-ask">Why you ask?</h2>
<p>Most of all, laziness prolly (okay, yeah it was laziness). And didn&#x27;t really know what I could put it on there. Blogs? Media hosting?</p>]]></content:encoded>
      <category>meta</category>
      <category>keroton.in</category>
      <category>domains</category>
    </item>
    <item>
      <title>this site exists now</title>
      <link>https://naft.pink/blog/hello-naft-pink</link>
      <guid isPermaLink="true">https://naft.pink/blog/hello-naft-pink</guid>
      <pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate>
      <description>Why I stopped putting it off and built naft.pink, and what I&apos;m going to use it for.</description>
      <content:encoded><![CDATA[

<p>I bought naft.pink a while ago and then did nothing with it, which is the normal
lifecycle of a domain. It pointed at a parking page for months. That got old.</p>
<p>So this is the replacement. One page that says who I am, one page listing the
hardware I actually use, and this notes section.</p>
<h2 id="what-goes-here">what goes here</h2>
<p>Dev logs, mostly. Short ones. Things like &quot;the VM broke in a funny way&quot; or &quot;I
picked X over Y and here&#x27;s the reason I&#x27;ll forget in three weeks.&quot; I&#x27;m not
writing tutorials and I&#x27;m not writing thought leadership.</p>
<p>The bar for posting is low on purpose. A post that&#x27;s four paragraphs and useful
to future me beats a post I never finish.</p>
<h2 id="how-its-built">how it&#x27;s built</h2>
<p>Next.js App Router, Tailwind, MDX for these posts. No CMS, no database, no
comment section. Posts are <code>.mdx</code> files in the repo, so writing one is the same
motion as writing code: edit, commit, push.</p>
<p>The whole thing is static. It could run off a folder on any web server, which
felt like the right amount of infrastructure for a personal site.</p>
<h2 id="the-pink">the pink</h2>
<p>The domain is <code>.pink</code>. Ignoring that would have been a waste. It&#x27;s a couple of
accents on a near-black background — links, a marker next to headings, a glow
behind the hero. Any more than that and it stops being readable.</p>]]></content:encoded>
      <category>meta</category>
    </item>
    <item>
      <title>writing a scripting VM in rust</title>
      <link>https://naft.pink/blog/writing-a-vm-in-rust</link>
      <guid isPermaLink="true">https://naft.pink/blog/writing-a-vm-in-rust</guid>
      <pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate>
      <description>Learning Rust by porting my game&apos;s scripting VM out of C++: a stack-based bytecode interpreter and a small compiler for gameplay scripts.</description>
      <content:encoded><![CDATA[

<p>I&#x27;m building a game in Unreal, and gameplay logic in C++ means a rebuild every
time I change a number. That loop is slow enough that I stop experimenting,
which is the worst thing a tool can do to you.</p>
<p>The fix is scripts. The excuse is that I wanted to learn Rust properly, so I&#x27;m
writing the whole thing myself: a small language, a compiler, and a VM to run
the bytecode.</p>
<h2 id="it-started-in-c">it started in C++</h2>
<p>The first version was C++, which made sense at the time — the game is already
C++, so the VM could just reach into game state directly. It worked. It was also
the kind of code where a crash three frames later was caused by something I did
three functions ago.</p>
<p>Rewriting it in Rust was supposed to be a learning exercise. It turned into the
version I actually use.</p>
<h2 id="stack-based-for-now">stack-based, for now</h2>
<p>It&#x27;s a stack VM. Every tutorial starts with a stack VM, and after the port I
never had a good enough reason to move off it — the interpreter is small, the
compiler is almost trivially simple, and none of my scripts are hot enough for
dispatch count to be the thing that hurts.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="rust" data-theme="rose-pine-moon"><code data-language="rust" data-theme="rose-pine-moon"><span data-line=""><span style="color:#3E8FB0">enum</span><span style="color:#9CCFD8"> Op</span><span style="color:#908CAA"> {</span></span>
<span data-line=""><span style="color:#EA9A97">    LoadConst</span><span style="color:#908CAA">(</span><span style="color:#9CCFD8">u16</span><span style="color:#908CAA">),</span></span>
<span data-line=""><span style="color:#9CCFD8">    Add</span><span style="color:#908CAA">,</span></span>
<span data-line=""><span style="color:#EA9A97">    JumpIfFalse</span><span style="color:#908CAA">(</span><span style="color:#9CCFD8">u32</span><span style="color:#908CAA">),</span></span>
<span data-line=""><span style="color:#9CCFD8">    Call</span><span style="color:#908CAA"> {</span><span style="color:#E0DEF4;font-style:italic"> func</span><span style="color:#3E8FB0">:</span><span style="color:#9CCFD8"> u16</span><span style="color:#908CAA">,</span><span style="color:#E0DEF4;font-style:italic"> argc</span><span style="color:#3E8FB0">:</span><span style="color:#9CCFD8"> u8</span><span style="color:#908CAA"> },</span></span>
<span data-line=""><span style="color:#9CCFD8">    Return</span><span style="color:#908CAA">,</span></span>
<span data-line=""><span style="color:#908CAA">}</span></span></code></pre></figure>
<p>The cost is obvious when you read the bytecode: <code>push</code>, <code>push</code>, <code>add</code>, <code>store</code>
for what is conceptually one operation. Registers are where I want to take it —
wider instructions, far fewer dispatches per line of script — but that means
writing a register allocator, and I&#x27;d rather have hot reload working first.</p>
<h2 id="what-rust-actually-taught-me">what rust actually taught me</h2>
<p>The borrow checker is the reason this works. The C++ version did the obvious
thing: hand the VM a pointer to the host game state and let it write wherever.
Rust refused to let me port that straight across, so I ended up with an explicit
host interface — the script asks, the host answers, nothing reaches across.</p>
<p>That boundary is the good part of the design and I didn&#x27;t come up with it. The
compiler forced it on me.</p>
<h2 id="still-missing">still missing</h2>
<ul>
<li>Garbage collection. Strings currently live forever. I&#x27;m going to pretend that&#x27;s a feature until it isn&#x27;t.</li>
<li>Debug info. A runtime error gives you a bytecode offset, which is useless. Line numbers next.</li>
<li>Hot reload, the whole reason for the project. Swapping a function&#x27;s bytecode mid-frame without invalidating live call frames is the interesting problem, and I haven&#x27;t solved it yet.</li>
</ul>
<p>Gets rewritten roughly every other month. That&#x27;s fine — the rewrites are where
the learning is.</p>]]></content:encoded>
      <category>rust</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
