Velger ukjente Zig: «Ikke lett å like Rust i praksis!»

Bun er kanskje det mest kjente prosjektet laget i Zig – men utvikler Michael Odden synes flere burde prøve det C-lignende språket.

Utvikleren Michael Odden valgte Zig fremfor Rust – men koder også i andre språk. 📸: Privat / Kurt Lekanger
Utvikleren Michael Odden valgte Zig fremfor Rust – men koder også i andre språk. 📸: Privat / Kurt Lekanger Vis mer

Av alle nye programmeringsspråk som dukker opp med jevne mellomrom, er Zig blant de som har fått mest oppmerksomhet de siste månedene.

Og det til tross for at språket faktisk ikke en gang er inne på TIOBE topp 50-listen, og på Stack Overflows undersøkelse i år var brukt av bare 0,83 prosent av utviklerne.

En viktig grunn til at enkelte utviklere har begynt å prate om Zig, er det nye JavaScript-kjøremiljøet Bunsom er skrevet i nettopp Zig.

Så hva i alle dager er Zig, hvordan skiller det seg fra andre språk, og ikke minst – trenger vi å bry oss om Zig?

Siden undertegnede knapt hadde hørt om Zig før Bun kom, så har vi spurt en som har greie på det: Michael Odden. Han jobber som selvstendig utvikler og koder mye i Zig, i tillegg til andre språk – gjerne innenfor systemutvikling.

«Zig kan veldig forenklet sees på som et forsøk på å lage et mer veldefinert C.»

– Michael, hva er Zig?

Zig er veldig kort fortalt et imperativt, statisk typet, generelt programmeringsspråk som blant annet legger til rette for systemutvikling, forteller Odden.

– Sånn sett overlapper det godt med bruksområdene til språk som C, C++, Rust og til dels Go. Uten at det trenger å stoppe der.

– Zig kan veldig forenklet sees på som et forsøk på å lage et mer veldefinert C. Valgene som tas på den veien er veldig meningsbaserte, og har og vil forbli gjenstand for mye diskusjon. Som med så mange språk!

– Andrew Kelley er å anse som initiativtaker og opphavsperson bak Zig, men prosjektet utvikles som et åpent kildekode-aktig prosjekt med Andrew som BDFN (Benevolent Dictator For Now). Det er etablert en non-profit organisasjon (Zig Software Foundation) for å forvalte donasjoner og med det lønne et knippe kjerneutviklere. Kildekoden er frigitt under MIT License (Expat).

– Hvor utbredt er Zig?

– Zig er en ganske liten sak utbredelsesmessig. Enkelt og greit!

– Av løsninger der ute av vesentlig størrelse og relevans er det jo veldig naturlig å nevne JS-kjøretidsmiljøet Bun som får en del oppmerksomhet for tiden og treffer bredt.

– Men selv synes jeg kanskje det mest spennende teknisk sett er TigerBeetle, en særdeles sikker og effektiv distribuert databaseløsning beregnet for finanssektoren. De er forøvrig flinke med å poste artikler og videoer som jeg vil argumentere for at er særdeles spennende uavhengig av hvilke språk og miljøer en jobber i.

– Det er en mengde mindre prosjekter der ute, for kommandolinjeverktøy, spill, kompilatorer, webservere med mer – men nok ingen som er å anse som allment kjente. Det er nok også verdt å bite seg merke i at Zig ikke har ankommet v1.0.

– Hvorfor har du selv valgt å kode i noe så "sært" som Zig?

Over en del år nå så har jeg sett bransjen generelt sett søke mot å fjerne seg fra tanken om at koden vi skriver til syvende og sist kjøres på noe maskinvare.

– Det er ikke et ugyldig mål i seg selv, men vi har på veien mistet perspektiv over hvor mye maskinvaren er faktisk kapabel til å yte – med opptil flere størrelsesorden – samt tillagt oss enorme mengder tilfeldig kompleksitet og feilflate ved å være overavhengige av tredjepartsløsninger.

– Vi legger nesten alltid på, og trekker nesten aldri fra. Jeg har derfor over tid søkt mot å skrelle vekk abstraksjoner som ikke skaper netto gevinst for meg.

– Hvordan skiller Zig seg fra andre språk?

Jeg tror nok flere vil finne det syntaksmessig nærmere C og Go enn Rust, men de har nok latt seg inspirere fra mange språk.

– Min opplevelse er at Zig er et forsøk på å ta et nytt blikk på hva C kunne vært. Heller enn å gå i retning C++ o.l. så fokuserer de mer på de grunnleggende byggesteinene og et begrenset sett av nøkkelfunksjoner. Og for meg som ordinært ville brukt C eller en veldig nedstrippet variant av C++, så synes jeg de lykkes ganske godt her.

– De skiller seg i så måte med at de blant annet har arrays med eksplisitt lengde, slices, defer, egen error-type med tilhørende krav til spesifikk håndtering, strengere typesystem og tilhørende konverteringsregler, gode compile-time kapabiliteter, et integrert test-system mm.

«Der zig kanskje har vært minst gode er når det kommer til pakkehåndtering.»

– I tillegg så krever de at du eksplisitt angir hva slags minneallokator(er) du ønsker å bruke, og kommer med et knippe varianter (page, arena, fixed buffer, ...) i standardbiblioteket. Alle standardbiblioteksfunksjoner som da trenger å allokere ekstra minne må da få en referanse til denne allokatoren sendt inn. Dette gjør riktignok koden noe mer verbos, men samtidig så blir det veldig tydelig når du gjør heap-allokasjoner, i tillegg til at det muliggjør en del snedige ting.

– For eksempel kan en unngå dynamisk heap-allokering helt ved å lage en allokator som kun benytter et predefinert minneområde definert ved oppstart. Dette kan eliminere hele kategorier av feil.

const std = @import("std");

test "allocation" {
    const allocator = std.heap.page_allocator;

    // Alloker et array av 100 bytes
    const memory = try allocator.alloc(u8, 100);
    defer allocator.free(memory);

    try std.testing.expect(memory.len == 100);
    try std.testing.expect(@TypeOf(memory) == []u8);
}

defer (også kjent i Go, men ikke implementert helt likt) sikrer at den tilhørende koden kjøres innen vi går ut av scope. Og ved multiple defers så vil de naturligvis løses opp i omvendt rekkefølge ift deklarasjonene.

– Andre skilnader er at det kan bygges fullstendig uten standardbibliotek, evt kun med de funksjonene du velger å benytte – og sånn sett generere veldig små binærer.

– Zig er også bundlet med sitt eget byggesystem der en definerer byggespesifikasjonen nettopp i zig. Ett mindre språk å holde styr på!

– Av andre forskjeller så er klassiske programmeringssetninger (statements) som ifs og switch faktiske uttrykk (expressions) slik at du kan tilordne resultat rett til en variabel:

const testing = @import("std").testing;

test "if som expression" {
    var value = if(true) {
        21;
    } else {
        42;
    };

    testing.expect(value == 21);
}

– Du kan også returnere fra blokker, som kan være kjekt både for å unngå ekstra variabler såvel som tilrettelegge for stegvis refaktorering:

const testing = @import("std").testing;

test "return fra block" {
    var value = blk: {
        var tmpvalue = 1+2;
        break :blk tmpvalue;
    };

    testing.expect(value == 3);
}

– Der zig kanskje har vært minst gode er når det kommer til pakkehåndtering. Utviklermiljøet rundt kom ganske tidlig på banen og lagde et knippe uoffisielle løsninger (f.eks. zigmod) som ulike prosjekter valgte å benytte en eller flere av. Dette synes jeg har skapt en del unødvendig støy.

– Nå relativt nylig – initielt til v0.11, og ikke ferdig – har de kommet med sin offisielle løsning, og det er ikke et øyeblikk for tidlig. På den positive siden så vil den også ha god støtte for å peke til "vendored" pakker hvis du ønsker å sammenstille tredjepartskode med din egen.

– Betyr Zig sikrere kode?

– For å adressere det først av alt; zig har ingen lånesjekker som i Rust, så den type garantier vil de ikke gi. Men for min del synes jeg de allikevel gir et godt nok sett med verktøy.

– Zig har et strengere typesystem enn for eksempel C:

test "null-eksempel" {
    var maybe_myvar: ?i32 = null; // ? sier at denne kan være null, og dermed må denne situasjonen håndteres.
    // Obs; denne testen vil feile
    if(maybe_myvar) |myvar| {
        // gjør noe moro med myvar. Evt forkast
        _ = myvar;
    } else {
        return error.CatastrophicError;
    }
}

– Alle variabler må initieres umiddelbart, med mindre du spesifikt setter verdien lik 'undefined', og uansett før du evt bruker dem:

// vil ikke kompilere
var myvar1: u32;

// ok..., men vil feile om du forsøker å bruke før du evt har gitt verdi
var myvar2: u32 = undefined;

– I tillegg har både arrays og slices en faktisk lengde assosiert med seg. Disse har da sjekker ved aksess, noe som eliminerer en kategori eller to av klassiske minne-problemer.

const std = @import("std");

test "array og slice-eksempel" {
    var myarr: [8]u8 = .{1,2,3,4,5,6,7,8};

    std.debug.print("myarr.len: {}\n", .{myarr.len});
    for(myarr, 0..) |val, i| {
        std.debug.print("{}: {}\n", .{i, val});
    }

    var chunk = myarr[0..3];
    std.debug.print("chunk.len: {}\n", .{chunk.len});
    for(chunk) |val| {
        std.debug.print("{}\n", .{val});
    }

    // vil evt gi panic:
    // myarr[8] = 9;
}

– Som jeg har illustrert i eksemplene så langt så er tester identifisert med nøkkelordet test og kan ligge fritt i kilden sammen med ordinære funksjoner slik at test og implementasjon kan følge hverandre tett:

// i foo.zig:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "her tester vi add" {
    std.testing.expect(add(1,2) == 3);
}

// i terminal:
$ zig test foo.zig

– Det er veldig tungt jobbe i prosjekter som krever separate filer og klasser for tester etter dette!

– Videre så kan du assosiere en spesiell error-variant for å beskrive feiltilstander en funksjon kan komme oppi, og hvis du benytter en potensielt feilende funksjon så må du ta stilling til dette:

const std = @import("std");
const MittFeilsett = error{UserError, SystemError};

// Utropstegnet i returtypen indikerer at denne kan feile, og venstresiden for denne forteller da hvilke feilenumereringer som kan oppstå
fn can_fail(shall_fail: bool) MittFeilsett!i32 {
    if(shall_fail) return error.UserError;
    
    return 123;
}

test "can_fail kan feile" {
    try std.testing.expectError(MittFeilsett.UserError, can_fail(true));
    try std.testing.expect(try can_fail(false) == 123);
}

test "feilsituasjoner må håndteres" {
    // Alternativ 1 er å pakke ut verdien ved sukses, eller propagere feilen videre - 'try' er kortformen for dette:
    {
        const myval = try can_fail(false);
        try std.testing.expect(myval == 123);
    }

    // Alternativ 2 er å håndtere feil - f.eks. kan du ha en standardverdi som settes
    {
        const myval = can_fail(true) catch 22;
        try std.testing.expect(myval == 22);
    }

    // Alternativ 3 å analysere den faktiske feilen og ta spesifikke valg avhengig av hva som har skjedd
    {
        const myval = can_fail(true) catch |err| blk: {
            switch(err) {
                MittFeilsett.SystemError => {
                    // Pay it forward / upward
                    return err;
                },
                MittFeilsett.UserError => {
                    // Default-verdi
                    break :blk @as(i32, 0);
                }
            }
        };
        try std.testing.expect(myval == 0);
    }

    // Evt la oss forenklet versjon 3 litt
    {
        const myval = can_fail(true) catch |err| switch(err) {
            // Pay it forward / upward
            MittFeilsett.SystemError => return err,
            // Default-verdi
            MittFeilsett.UserError => @as(i32, 0)
        };
        try std.testing.expect(myval == 0);
    }
}

– Denne feilhåndteringen baserer seg da ikke på exceptions med all dens problemstillinger. Riktignok må du selv bygge det hvis du virkelig trenger å sende med mer innformasjon enn i praksis en enumeratorverdi.

– Zig definerer også veldig klart – samt kravstiller – at du forholder deg ryddig til bitbredder, signed vs unsigned situasjoner og gir deg mulighet til å enkelt ta eksplisitt stilling til klassisk integer overflow-problematikk.

– En kan også bygge sin egen sikkerhet ved å selv implementere vilkårlige kompileringstid-sjekker i funksjoner:

const std = @import("std");

fn add_user(comptime user_id: u32) bool {
    comptime {
        if(user_id < 1024) {
            @compileError("user_id must be >= 1024");
        }
    }
    
    // ... logic logic logic
}

– Eksempelvis bruker jeg kompileringstid-kapabiliter for et spill jeg jobber med til å først inkludere alle ressurser (teksturer og fonter) direkte inn i binæren slik at det kun er snakk om èn enkelt fil å deploye, klar til kjøring, og videre sjekker jeg at alle fil-stiene jeg da refererer til andre steder i koden faktisk er gyldige. Alt på kompileringsstadiet.

– Hvorfor velger du Zig fremfor Rust?

– Det er helt klart et stort overlapp på bruksområder mellom Rust og Zig!

– Jeg liker Rust veldig godt i teorien. Med bakgrunn fra safety-kritiske sanntidssystemer så var det mye med Rust som kilte meg på de rette stedene og ting lå egentlig til rette for en god relasjon.

– Men Rust i praksis viste seg å ikke være spesielt lett å like totalt sett, og ikke kompatibel med hvordan jeg foretrekker å utvikle programvare:

– Jeg vet for eksempel ikke nødvendigvis hvordan minnemodellen min ser ut når jeg starter. Jeg bygger litt, itererer, stokker om, flipper på arrays av structer til structer av arrays, splitter structs i nye structs og så videre. Sett bort fra at det er enkelte minnemodeller som ikke praktisk lar seg implementere i Rust per min forståelse, så innførte for eksempel lånesjekkeren – for meg – for mye friksjon til at det var noe gøy lenger. Og jeg liker å ha det gøy.

«Lånesjekkeren i Rust innførte for mye friksjon til at det var noe gøy lenger. Og jeg liker å ha det gøy.»

– Videre synes jeg makro-løsningen er uelegant å forholde seg til, samt at kompileringstidene – ihvertfall sist jeg testet – var uforholdsmessig lange. Jeg anerkjenner selvsagt det at en økt mengde kompileringstidsgarantier vil komme med en kostnad. Men det er for meg en avveining som for meg for de fleste tilfeller bikker i feil retning.

– Et av prinsippene til Zig er at det skal ikke være noen skjult programflyt. Det vil si; hvis du ikke leser noe kode som spesifikt ser ut som et funksjonskall (en identifikator etterfulgt av paranteser) så skal du være helt trygg på at programmet heller ikke hopper videre noe sted. Dette kan være tøft å svelge for noen, og jeg ser selv at det finnes et lite utvalg situasjoner der f.eks. operator-overloading har en viss verdi, men alt i alt verdsetter jeg dette prinsippet.

– Zig derimot viste seg for meg å ha et godt nøkkelsett med kjernefunksjoner jeg verdsetter, og har fortsatt en sunn mengde friksjon for å rettlede til å skrive trygg nok kode.

– I tillegg er C-interopabiliteten fryktelig bra. Det er trivielt å inkludere C-biblioteker, og jeg er tilbøyelig til å argumentere for at de gjør det bedre enn C selv.

– Zig gjør det for meg enklere å skrive kode hvor jeg er trygg på ytelse, ressursavtrykk og riktighet. Både i utviklingsperioden såvel som når jeg i ettertid må gjenbesøke eldre kode.

fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "add" {
    const value_comptime = comptime add(1,2);
    const value_runtime = add(1,2);
    // forkast/ignorer verdier
    _ = value_comptime;
    _ = value_runtime;
}

– Hva egner Zig seg best til?

– Jeg vil si Zig er spesielt godt egnet der ytelse og kontroll teller, uten at det trengs å begrenses til det. Zig benytter pr nå LLVM for release-bygg, og har tangerende ytelse med f.eks. clang's C og C++.

– Her snakker vi for eksempel om spill, native applikasjoner og embedded. Det kan forøvrig også enkelt bygges mot webassembly, så jeg har brukt det for å gjøre ett av mine native-verktøy tilgjengelig via nettleser.

– Hva bruker du det til selv?

For min del har Zig tatt over alle steder jeg ellers ville brukt C og C++ (eller Rust for den saks skyld), samt flere steder der jeg tidligere brukte Python eller C#.

– Zig har vært mitt foretrukne språk der jeg ikke har hatt eksterne krav som sier noe annet siden høsten 2021.

– Jeg bruker Zig primært til å lage ulike kommandolinjeverktøy og spill. I tillegg har jeg brukt det for å lage en liten kompilator, samt at jeg arbeider med en liten webserver. Det har også vært min go-to i forbindelse med kodekalendere (hei til blant annet Knowits kodekalender!) de to foregående årene.

– Jeg leker med tanken om et par særere brukstilfeller, men det kan vi heller komme tilbake til!

– Hvor vanskelig er det å lære seg Zig?

– Her kan jeg nok best svare for meg selv i og med at dette vil avhenge av hvilke type språk en er kjent med fra før.

– Men det er få eller ingen av funksjonene i seg selv her som er unike eller grensesprengende om en har vært eksponert mot litt ulike språk fra før.

– Med min bakgrunn fra blant annet C så synes jeg det var grei skuring. Selve syntaks-utvalget ikke enormt, noe som gjør det mulig å faktisk ha det i hodet. Der det ligger mer jobb er jo i å bli godt kjent med standardbiblioteket.

– Jeg kan tenke meg at jeg var til en viss grad produktiv på et hobbyprosjekt innen en uke med å se på Zig nå og da på kveldstid.

– Har du tips til andre som vil lære seg Zig?

– For å få en liten forsmak på syntax så kan en jo skumme igjennom denne:

ziglang.org/learn/samples/

– Deretter så finnes det veldig enkle steg-for-steg-guider for å komme igang her:

– Videre så synes jeg selve språkreferansen er godt lagt opp – alt som en enkel HTML-side. Greit å søke seg frem om det skulle være noe:

ziglang.org/documentation/master/

– Jeg synes også denne tilnæringen hvor du skal fikse små ødelagte programmer er festlig (inspirert av rustlings):

codeberg.org/ziglings/exercises/

– Deretter vil jeg vel anbefale å se på koden til noen ekte prosjekter – for eksempel standardbiblioteket til zig selv. I og med at zig kommer med standardbiblioteket i kildekodeformat når du installerer kompilatoren, så kan du enkelt med språkserverstøtte bare gå til definisjon og lese kode og tilhørende tester med enkelhet.

– De har også en Discord, på discord.gg/zig, samt en Reddit-like side, på zig.news/

«Du bør nok tenke deg om både 4 og 5 ganger før du går i produksjon med dette.»

– Noe annet du vil fortelle kode24-leserne om Zig?

– For meg har Zig-kodebaser vist seg å være de kanskje enkleste kodebasene å komme tilbake til i ettertid. Jeg tror mye av grunnen til det er at all programflyt er rett i trynet på meg, og ekstrajobben initielt sett med å skrive litt mer ekspressiv kode sånn sett gir god uttelling i etterkant når en skal lese og manipulere det.

– Det er ikke til å komme fra at Zig vil tvinge deg til å skrive noen flere linjer kode enn mange klassiske høynivå-språk, men jeg har selv funnet at jeg ikke har gått noe bemerkelsesverdig ned på produktiviteten av den grunn. Rask bygging, smidig test-iterering og eksplisitt kode har nok veid godt opp for dette.

– Verktøystøtten og utvalget av tredjepartsløsninger er fortsatt i en gryende fase. Du bør nok tenke deg om både 4 og 5 ganger før du går i produksjon med dette, men min erfaring er at dette er på vei til et sted jeg liker veldig godt. De har en LSP-server og utvidelser for de største generelle editorene så det skal være greit å komme igang.