Hexz, naar het begin

Artikelen · Experiment

Eén krabbel, tien talen

Dezelfde tekenapp, tien keer gebouwd. De kleinste is 383 bytes, de grootste zeven megabyte.

Wat ik me afvroeg

Wat kun je eigenlijk met WebAssembly?

Ik wist dat Figma er veel mee doet. Ik wist ook dat je bestaande code in C++ of Rust er vrij makkelijk naar kunt omzetten. En ik zag steeds vaker dat code wordt herschreven in Rust, omdat dat veiliger zou zijn dan C++.

Dat wilde ik zelf zien. Maar Figma nabouwen is voor een experiment veel te groot. Dus greep ik terug op iets wat ik al kende.

Een krabbel uit 2005

In mei 2005 maakte ik een variant op de klassieke krabbel: je tekent een lijn, en die lijn vervaagt achter je aan. Alleen het laatste stuk blijft staan.

Dit is de code van toen, in C#:

private const int HIS = 40;
private Point[] _p = new Point[HIS];
private int _t;

private void OnMouseMove(object sender, MouseEventArgs e)
{
   if (fMouseDown)
   {
      if (_t<HIS-1)
         _t++;
      else
         _t = 0;
      _p[_t].X = e.X;
      _p[_t].Y = e.Y;
      this.Refresh();
   }
}

Veertig plaatsen, en een teller die bij het einde weer vooraan begint. Het nieuwste punt schrijft over het oudste heen. Bij het tekenen loop je terug vanaf de teller, en elk stuk wordt iets lichter:

myColor = Color.FromArgb((255-(i*6)), Color.Black);

Dat heet een ringbuffer. Dat wist ik toen niet.

Wat ik deed

Het was ook een experiment met AI. Vroeger had ik tutorials doorgewerkt en alles zelf moeten ontdekken. Nu vroeg ik om een werkende opzet, en die stond er in één keer: de krabbel uit 2005, in Rust, in de browser.

In dat gesprek kwam ook het voorstel om het in andere talen te proberen. Zo werden het er tien:

De afspraak is voor elke taal dezelfde. Onthoud de laatste honderd punten van de pen. Komt er een punt bij, dan valt het oudste eraf. En hoe ouder een stuk van de lijn, hoe verder het vervaagt.

Dat zijn acht kleine functies: een punt erbij, alles wissen, en zes om de punten weer uit te lezen. De pagina tekent, de taal onthoudt. Daardoor kun je in het tekenvlak van taal wisselen terwijl je krabbel blijft staan: de punten verhuizen gewoon mee.

Wat me verbaasde

Hetzelfde, maar achttienduizend keer zo groot

De grootte van wat de browser moet ophalen, onverpakt. De schaal is logaritmisch: elke stap naar rechts is tien keer zo veel.

De handgeschreven versie is 383 bytes. Dat is ongeveer zo veel als zestig woorden tekst. De versie in C# doet precies hetzelfde en is ruim zeven megabyte.

Het verschil is wat er meereist

Voor een krabbel schieten C# en Go hun doel voorbij. Ze brengen een heel huis mee om één lijn te onthouden.

De taal bepaalt niet hoe groot je programma is. Wat je ongemerkt meeneemt, bepaalt dat.

De grens oversteken kost ook iets

Elke keer dat de pagina een punt doorgeeft, steekt ze de grens over tussen JavaScript en WebAssembly. Hoe duur dat is, verschilt per taal. Druk bij het tekenvlak op meet: dan gaan er honderdduizend punten doorheen, op jouw apparaat. Wissel daarna van taal en meet opnieuw.

Op mijn computer duurt dat in de meeste talen een paar milliseconden. In Go ruim een seconde: daar loopt elke aanroep via een omweg door JavaScript.

Het dichtst op de machine leest het moeilijkst

Het Windows-programma in C++ zou het snelst moeten zijn: er zit niets tussen de code en het systeem. Dat heb ik niet gemeten. Wat ik wel zag: het is de variant die ik het minst makkelijk lees.

De versie in C is 62 regels. De versie voor Windows is er 317. Het meeste daarvan gaat niet over de krabbel, maar over de afspraken van het systeem: vensters aanmaken, berichten afhandelen, namen als LRESULT CALLBACK en GET_X_LPARAM.

In één keer een werkende opzet

In 2005 kostte de krabbel me een paar avonden, in één taal. Nu stonden er in een paar dagen tien varianten. Niet omdat ik tien talen ken, maar omdat ik wist wat het moest doen en dat kon uitleggen.

Dat verandert wat een experiment is. Vergelijken kost bijna niets meer. De vraag "hoe zou dit in Zig gaan?" hoef je niet meer te laten liggen.

Hoe het werkt

Dit is de hele functie die een punt toevoegt, in Zig:

export fn push(x: f64, y: f64, connected: i32) void {
    const h: usize = @intCast(head);
    xs[h] = x;
    ys[h] = y;
    conn[h] = if (connected != 0) 1 else 0;
    head = @mod(head + 1, MAX_POINTS);
    if (count < MAX_POINTS) count += 1;
}

En dezelfde functie in C:

void push(double x, double y, int connected) {
    xs[head] = x;
    ys[head] = y;
    conn[head] = connected ? 1 : 0;
    head = (head + 1) % MAX_POINTS;
    if (count < MAX_POINTS) count++;
}

Leg ze naast de code uit 2005. Het is hetzelfde idee: een rij met vaste plaatsen, en een wijzer die rondloopt. Is de rij vol, dan schrijft het nieuwe punt over het oudste heen.

De browser krijgt een bestand met deze functies, zet het klaar in een afgesloten stukje geheugen en roept ze aan alsof het gewone JavaScript is.

De code

De code komt openbaar op GitHub. Binnenkort.

Gemaakt met hulp van AI. De tien varianten, het voorbeeld op deze pagina en de tekst zijn met een AI-assistent gemaakt. De krabbel uit 2005, het idee, de vragen en de keuzes zijn van mij.