intro-to-zig-talk/talk.md

10 KiB

Intro

Before we start:

  • might be running slow - streaming + old laptop
  • no slides, just text
  • live coding demo
  • will try to cover most cool things
    • but no multithreading/async/atomics and C interop details in talk

Who am I?:

  • Ivan Notaroš - nothke
  • indie gamedev for 10+ years
  • studied architecture
  • started by modding - 3D modeling
  • Unity, C# since ~2013
  • many game jams: https://nothke.itch.io/
  • Unity had too many limitations, esp for procgen, progressively went lower level
  • custom engine in C++ since 2020
    • making a rally game: Shakedown Rally in it
  • heard of Zig a few years ago
    • kept hearing of cooler and cooler features
    • started making an engine just to see how easy it would be to move C++ to Zig
  • I stream gamedev on twitch too: https://twitch.tv/nothke
  • Windows user all my life 😢

Zig

  • Hard to put in an elevator pitch
  • Fell in love with language and community - https://ziglang.org
  • Don't know where to start, I'll try my best

Zig (language) facts:

  • Zig is an imperative,
  • structured,
  • general-purpose,
  • statically typed,
  • compiled,
  • system programming language

History:

Goals:

  • "better C"
  • simple, clear, readable
  • very explicit
  • maintainable
  • robust
  • sane defaults
  • fast: runtime AND compile
  • no hidden allocations
  • write all in Zig
  • minimal dependencies
  • no UB in debug or safe
  • unambigous, context-free syntax
  • one obvious way to do things

Some really cool bits (apart from already mentioned):

  • all-in-one cross-compiling toolchain
  • include and seamlessly interoperate with C code
  • comptime - powerful metaprogramming - written in zig
  • integrated build system - written in zig
  • no hidden allocations - easy to reason about memory
  • amazing standard library
    • which is completely comprehensible! (unlike C++ STL 😛)
  • decentralized package manager
  • zls
  • pointer disambiguation
  • no exceptions - error types
  • no header files (!!!)
  • bit-packing types
  • SIMD vectors
  • no function or operator overloads
  • strong formatting convention
  • OOP-lite
  • no macros
  • if it looks like a function, its a function. Fields are just fields!

The toolchain

  • all-in-one
  • cross compile from-to any system
  • compile C and C++
  • include C code
    • translate-c - C to zig
    • -ofmt=c - zig to C
  • very fast
  • build system written in Zig
    • just zig build run
    • no CMake!
  • integrated tests
  • 4 compiler modes --release=[mode] :
    • debug
    • safe
    • fast
    • small
  • lazy compiler
    • only compiles what you use

Live demo!

installing

  • with VSCode:
    • install Zig extension
    • ctrl+shift+P
      • install zig
      • install zls
  • alternatively:
    • with zvm https://www.zvm.app/
      • zvm i --zls master or 0.13.0 (for now)
    • install binary
      • build zls
    • sudo apt install zig

Language walkthrough

const and var

  • : T =
  • initialization
  • const - immutable
    • const resolved at compile time if it can be
    • can act as an alias to types
  • var - mutable

integers

  • unsigned: u8, u16, u32..

  • signed: i8, u16, u32..

  • but also arbitrary width: i2, u6, i31..

  • casting - peer type resolution

    • narrow to wide - allowed
    • wide to narrow - disallowed, must be explicit @intCast
      • if compile-time known - casting checked at compile time
  • overflow is an error

    • but explicit overflow operator +%, -%
  • Note: const a = 1; is a comptime_int

  • floats f16, f32, f64, f128 and f80

pointers (single item):

  • *T
  • & - address of operator
  • can't be null
  • (..we'll come back to more pointers..)

[N]T - arrays

  • on stack, compile-time known size
    • size part of type
  • [3]T
  • [_]T - inferred size from initialization
  • unambiguously values
    • unlike C which pretend to be pointers

[]T - slices

  • const slice: []T = &arr;
  • or const slice = arr[0..];

strings

  • what strings..? They're just []u8
  • "string class" not needed, std.mem.- functions are rich enough for every need
  • For unicode there's std.unicode.-

++ ** - compile-time array operators

  • arr ++ arr2 - concatenates
  • arr ** 3 - repeats

structs

  • const MyStruct = struct {}
  • fields
    • always public
    • rearranged to better fit memory
      • unless extern
    • can be default initialized
  • are namespaces
  • namespaced functions
    • if first parameter is MyStruct, acts as a "method"
    • if self needs to mutate, pass as pointer self: *MyStruct
  • initialization
    • uninitialized fields not allowed
      • (but fields can be default initialized ^)
    • force uninitialized with = undefined
      • explicitely shoot yourself in the foot! (unlike C)
  • files are structs too!
  • packed
    • bools become 1 bits
    • arbitrary-sized ints become bit-sized
  • no ctors/dtors - more on that later..

enums and unions

  • enums
  • (bare) unions
    • known which type
    • no type punning
  • tagged unions
    • use a backing enum
    • switch
    • polymorphism on stack!
  • ^ enums and unions also act as namespaces that can have functions! 😉

Zig has no operator overloads!

  • = is always a (shallow) copy
  • [] only works on arrays, slices and multi-item pointers
  • & always a pointer to - looking at C++

But I'm a gamedev and really need my vector arithmetic overloads 😭

  • Don't worry! There are built-in Vectors! 😍

@Vector(3, i32)

  • SIMD out of the box
  • can index
  • cannot loop through
    • but can cast into compatible array [3]f32 = @Vector(3, f32)

functions

  • fn
  • parameters always const
    • compiler can infer copy or const-ref, depending on size
    • params
  • return must be known
    • but comptime expressions possible
  • function pointers

control flow

  • if (bool)
  • while (bool)
  • for (array) |x| {}
    • |x| capture bars
      • expression creates variable
    • range 0..
    • multi-item
    • inline - unrolls loops
  • switch
    • exhaustive

!?

  • !T errors and ?T optionals
  • ! error unions
    • error enum
    • type example: divide by zero
    • handling errors:
      • try
      • catch
      • switch
  • ? optionals
    • can be null
    • ?*T - nullable pointers
      • don't consume extra space
    • opt.? unwraps value, if null throws error
    • opt orelse 0 if null use another value
    • if (opt) |value| {} if not null, unwraps
    • while (opt) |value| {} runs until null
      • can be used to implement iterators while (iter.next()) ..

Disambiguation of Pointers

  • in C/C++, a pointer can be

    • dereferenced *ptr
    • indexed ptr[3] - and not bounds checked
    • added to ptr + 1
    • nullptr
    • not known if it points to a null terminated string..? 🤷‍♂️
  • Zig disambiguates all these and puts them in separate types:

    • *T - single-item pointer - cannot be indexed
    • []T - slice - bounded, cannot be dereferenced with t.*
    • [*]T - multi-item pointer - unbounded
    • [:0] - sentinel-terminated, 0 in this case
    • ?*T ?[]T - nullable
    • *anyopaque - equivalent to void*
    • *align(1:0:1) T - pointers can also hold alignment and bit offset in their type
    • [*c] - can do all - but use it only for interoperating with C code!
  • Why? intent is much clearer!

Comptime

  • no macros, no templates, no constexpr
  • call any function at compile time!
  • types are types
    • (at compile time only)
    • can be used to return types to create generics
      • example: Optional(T)
  • full static reflection
  • anytype
    • function parameter type that can ONLY be inferred at compile-time
    • similar to template function auto parameters in C++
  • comptime var
    • can be mutated but only at compile-time
    • acts as const at runtime

Memory Management

Memory management in other languages:

  • manual (C) - malloc() free()
  • RAII (C++)
  • GC (Java/C#/python/Go)
  • compile time resolved (rust)

Zig - manual, BUT, unlike C:

  • defer
    • executes at the end of the scope
    • like RAII - BUT controlled by the user
    • errdefer like defer but only when error is thrown
  • has allocators

Allocators

  • interface used in std with anything that allocates heap memory

  • PageAllocator

    • the "dumb" one
    • makes syscalls
  • FixedBufferAllocator

    • provide a buffer, can be on stack
    • not resizeable
  • GeneralPurposeAllocator

    • can catch leaks in debug
  • ArenaAllocator

    • can discard entire chunks
  • TestingAllocator

    • only available in tests
  • Demo how to use with std.ArrayList

Built-in Tests

  • test "My Test" {}
  • std.testing.expect-
  • just zig build test

Package manager

  • build.zig.zon
  • not centralized
  • hash to check code correctness
  • zig fetch --save "link"

Cons

Community and Resources

Future of Zig

  • goal of 0.14.0: super fast compile times
  • getting rid of LLVM:
    • with LLVM 170MB, without 4.4

Conclusions

  • wHeRe dOeS zIg fIt iN tHe mOdeRn wOrLd??
    • Aren't you convinced by the talk..?
    • Reduces friction
  • Companies already using Zig:
  • zig zen
  • What about Odin, jai..?
  • Will do the same talk in Hamburg for code.talks

Epilogue: