Hello everyone My name is Philip And I'll talk today about the Zig programming language! (website scrolling then start zig pres) Zig aims to be a better C, nothing more nothing less which means about no real innovation, it is quite verbose (but still less than the C programming language itself) and it adds very few functionnalities Guess what? That's why it's great! ## Warning Before anything. I'll assume you have some programming background, and you know at least the basics of the C programming language. Also, Zig isn't production ready, yet. The Zig team wants more fellow compiler developers, to help them finish it. They aren't intereted in new users. That's why... there isn't much documentation, yet. So keep in mind that, if you like what you see, you'll have to read the standard library source code to know how to use it. But hey... I did it, so you can too. And from my experience, both source code and developers are very friendly. ## Presentation In this presentation I'll draw a quick overview of the Zig project Then we'll jump on a lot of examples to show differences between Zig and C ## Why comparing Zig to C and not X If you want to know why I don't compare it to Rust or C++, there is a webpage dedicated to this. Mostly, Zig aims to have a simpler and more readable language. Since I'll probably explain to you most of what you need to know, in order to start programming in Zig just with this video, I'm quite confident it's true. (sorry C++ and your dozen ways to fuck up) For Go, Haskell, Python... they cannot be a replacement for C for technical reasons. Mostly, memory management (some usages aren't compatible with a garbage collector). ## C has a massive techical debt So. Let's talk about C. C has a lot of pitfalls and arbitrary language limitations Its standard library is limited (extra libraries are necessary for most tasks) C development environment and tooling are quite clunky and dispersed Sharing code isn't as easy as it could (as with npm, go, etc.) C needs a preprocessor and it leads to many problems (we'll see that later) C has multiple and incompatible C libraries, leading to difficulties to port programs from one system to another Even on the same plateform there can be difficulties. Alpine Linux for example is entirely compiled with musl instead of glibc and they have to patch a lot of applications in order to do so. Yes. C is more than 50 years old. A lot has changed between then and now. I think that we have enough hindsights now to do better. One way, as we'll see, is to create a new (but very familiar) language. Another way may be to change the C language, as the people behind libc-coord mailing list are doing. Since we won't rewrite 50 years of applications, that's a valid approach. If you are interested, checkout the link in description. Finally, Zig! Here is a list of a few things that I think are nice even if they don't represent a good enough reason to switch to zig. Don't worry, I'll be fast, but we will see most of this in practice soon. Zig compiler is a C and C++ compiler, with different c libraries included which means you can compile C and C++ code for any architecture and any operating system (if your code is portable enough). Zig is C compatible, so you can write a library that can be used directly in C, or the other way around. No FFI, or bindings or whatever. Cross compilation is awesome This line in the only thing you need to have your code working on a never-ending list of OS and architectures. I'll give some examples later. ## ZIG OVERVIEW 2 Now are the game changers... to me. We will everything in details. First, namespaces. When you import a library, it will be put into a constant. Dead simple, yet, big improvement. Optionals! An optional is a value that can exist or not, and it is better than using null pointers. It reminds me of Maybe in Haskell. Tests are a thing. You can now test your libraries or just some functions in your code just by using this keyword and a simple block of code. Zig doesn't need a preprocessor. The language has generic structures and reflection without macros. Then, error management in Zig is made through enumerations of errors, not integers. And users are forced to take errors into account, so you can be way more confident in the code. We'll see that. In Zig, we can defer a block of code at the end of the function (or scope). INSERT HEAD FULLSCREEN This may not seem much, but combined with errors and memory allocations, it makes wonders. Memory allocations are now managed in a modular way, with 'allocators'. INSERT keywords: raw_c_allocator page_allocator HeapAllocator WasmPageAllocator StackFallbackAllocator FixedBufferAllocator GeneralPurposeAllocator LoggingAllocator ArenaAllocator These are abstractions to memory allocations, to handle them in different ways. By using a fixed buffer and not allocate any heap memory, or trying to limit allocations or recording when allocations are made and freed to verify double-free errors or leaks... Some allocators have a thread-safe version, too. That is clearly awesome to me... and at the same time, simple. Finally, the standard library brings a lot of goodies. OS abstractions, basic and not so basic structures, and allocators I just talked about. And we can say that nothing really is new. We saw these features in many other languages before. But not this whole package, and it always comes with counterparts when you just want a C successor. So yes, Zig is C without the bullshit ## Examples Let's dig into some simple examples of Zig vs C. We will see that both are very similar. -- hello In C, we have an include and the main function. That's almost the same for Zig, except that all imported code is contained into the constant 'std'. This is a simple namespacing. Then, our public function main, returning void, contains a single function call: std.log.info Parameters are a format (our hello world) then the list of things to print, which is empty (as in the printf in C). By default, Zig provides several ways to print stuff. In this case, I used the log.info function, but I could have used std.debug.print, for example. I won't get into details, Loris Cro did a video on this. INSERT: "print" function A thing to remember: Zig doesn't provide a default print since there are different considerations. INSERT: thread-safe? For example, do you want your print to be thread-safe? INSERT: buffering? Or, do you need buffering? Answers to these questions won't be universal. So you have std.log and a debug print with some assumptions, and write your own if it isn't enough for you. https://www.youtube.com/watch?v=iZFXAN8kpPo -- numbers Next, simple numbers. Nothing much changed from C, except for default types. In C there are int and float, but they aren't great since you don't know their size: they change depending on your operating system or architecture. We often use other types defined in macros: int16_t or uint32_t etc. In Zig you can tell how many bits you want directly. Printing values doesn't require you to put the type in the format, that computed at compile-time. Also, for integers, you can ask for arbitrary sizes. In this case, I create a variable, named 'myvar', that is an unsigned 10-bit integer. As I put in comments, this can be great for reading and writing network packets, for example. Also, writing big numbers is easier in Zig since we can put underscores in values. And it's a bit more consistent. -- overflow Next, we see simple operators. No big changes from C, except for one (good) thing. In C, overflows like this one are tricky for the unadvised. In this example we have an unsigned integer with a zero value, and we substract one. This isn't a valid operation mathematically, result should be an error. In C, the result is based on hardware implementation details. In Zig, the same operation produces an error. But since the C behavior actually is useful, new operators are implemented called 'wrapping operators' (addition, substraction, etc.). This is great, now we know the difference when we allow overflows or not. -- booleans Next, booleans. In C, the boolean type was added in C99. Nothing much to say, INSERT: macro true and false except that true and false are just macros to 1 and 0, INSERT: true + 1 = 2 meaning that true + 1 equals 2 (absurd), INSERT: #defined bool _Bool and that 'bool' type actually is underscore Bool. Again, as we saw with integers, basic types in C have so much history. Including headers to have basic types, that may be a macro to 1 or 0, is silly. That's 50 years of computer history for you. In Zig, we have the same thing, except for boolean operators 'and' and 'or'. I like keywords so that's fine to me. -- arrays Next, arrays. In C, we declare a static array of 5 values this way. Beware that we can use sizeof to know the size of the array in memory, but it can be hacky to compute its number of elements (and it only works for static arrays). INSERT: sizeof array And you can write this: sizeof array but not this: sizeof int So much whacky stuff here. In Zig, an array with undefined content is declared this way. It is a variable, named 'array', with a type of array of 5 elements of type u8. Content is undefined. You can get the number of elements of an array with dot len, static or not. An array with defined values can be written this way. The size of the array is computed at compile-time, so you can just put an underscore here. Another way of defining an array is this. I admit, this is less obvious to read at first. This version allows you to put a value for each element, and you can call a function to get it. Next we want to loop over values of an array. In C, we create an index from 0 to the max number of elements that we computed earlier, classic. In Zig you can just get each item of your array this way, or with an index, or, if you want to modify the value of the elements, you can ask for a pointer on the items. This is mostly syntactic sugar, but you can communicate the intent. Next, let's see what we can do with array elements. In this example in C, I don't put default values in this array. So, I set and use the first element, but when I access the second element it is garbage. We can have the exact same things in Zig. Next, we have an array, and ask for an element outside its range. In C, this leads to a garbage value, or a segfault if you are lucky (at least you know there is a problem). In Zig, this is a compilation error, and this happens at runtime, you have a runtime error (not catchable). So, in Zig arrays are simpler to use, and you have less conditions to write for the same result. If you get something wrong, you'll know it: errors are better than garbage values or segfaults. -- pointers Next we have pointers, and there isn't much to tell. Syntax is very slightly different. -- slices Next, we have slices. They are just a pointer and a size. You can see them as arrays with only a runtime known size. Nothing much to it, still, this simplifies the code. -- strings Next, strings. As always for static values, you don't need to write the type in Zig. If you want though, the type here is a simple array of constant unsigned 8-bit integers. There is no zero value at the end of the string as in C. If you want that, maybe for compatibility, you need a 'sentinel' value. Let's see an example of C compatibility. It is a pointer on a constant array of five elements of type unsigned 8-bit integer, and there is a sentinel value of 0 at the end of the array. In any case, you'll get the right value (five) when asking for the size. No need for this 'strlen'. -- optionals Next, something that doesn't exist in C: optionals. An optional is a value that can exist or not. For example, in Zig here I have a variable named 'optional_value' which is an integer but that may not exist. Right now, this value doesn't exist, it's null. Beware, that's not 'null' as in C, this is not zero. In Haskell you will do that with a Maybe. And to check if there is a value, you can do a conditional like this. In C, you have to hack a bit. Let's say that you have a variable x, and the function named 'some_function' MAY change it. To be sure, you need another variable so 'some_function' can tell you if the value was changed or if it garbage. Another way is to return a pointer, and if it is null then you don't have a value. Either way, it's more complicated than it has to be. -- structures 0 Next, we'll see structures. In C, structures are simple, but very limited. In Zig, as always the syntax is similar. In this example we can see two main differences. First, structures are just a value that you put into a constant. No special treatment. Then, we can have default values. -- structures 1 Next, structures can hold public constants, not only attributes to each instance. A structure in Zig isn't just a wrapper over a buffer in memory for each instance. Also, they can have functions, and that'll be very useful. It helps organize the code a little bit, create namespaces. There is no absolute need to have object oriented programming, and having just namespaces actually is enough (at least for my needs). -- structures 2 Next, we'll see our second built-in function: @this. This, refers to the current element you are in, the current structure. For example, we are creating here a Point, and within its definition, we can refer to it as @This. In this example, we use a constant Self to refer to the current structure. Having a Self constant within a structure is a common practice. Self, is later used in the function set for the first argument, a pointer on the current structure. Let's see how this structure is used. First, we create an instance of structure Point named 'p'. Then we use the 'set' function we saw earlier. As we can see, the first argument of the function is implied to be the 'p' variable, our current instance. That's because we have a pointer on Self as the first argument. This syntax isn't a big change, still, it is simpler to use than in C. This adds namespaces and then function names become simpler since they are binded to the structure they work on. Simple, not new by any mean, yet powerful. -- back to presentation - a break to enjoy C preprocessor Now, we saw that C and Zig aren't much different, and even if the syntax changes a bit it still is very familiar. Zig is easier to write and read, despite doing almost the same thing. Zig does add extra instructions like storing array lengths, but you had to do that anyway so it was only logical to do that automatically and IN A CONSISTENT WAY. Now, let's take a break and enjoy C preprocessor, shall we? -- macros There. Read this code and guess what it does. Sure, we are TRYING to get a safe divide of x by y. Annnnnnnd it doesn't work. This macro is translated this way, which clearly isn't what was intended. -- calling macros Next, a very simple macro SQUARE. I let you a sec to read it. Now, what could go wrong? Yes, this will compile, yet this won't work as expected. -- evil macro Next, guess what this program does. It does seem very simple. In fact, you cannot know. Everything here could have been changed in 'something.h'. Let's see what's its content. There, I redefined 'if', 'if' now is a macro. And it could have been even worse, let me introduce to you 'evil.h'. Yeah... that's nasty. Moving on. -- forget purity Next, we have a very simple macro, a simple instruction saying that x equals zero. But, when you use it, you don't see that it changes a variable in your local scope... You just see a function call. And you know what? With macros, you can get C to a whole new level. -- lib cello What do you know about C? A very old imperative programming language? Naaaaah! It's actually an oriented-object programming language, with generic data structures, (you can have) polymorphic functions, (you can have) interfaces, (everybody will get) exceptions and reflection!! Thanks to libCELLO! They even had clojures at some point... But there are _some_ cons, like NO TYPE CHECKING. Thanks, but no thanks... SATAN So now that we saw what could be done using macros, mostly things we don't really want since we cannot control debugging and other MINOR stuff like that, let's see how we can replace them. -- comptime 1 Let's see something familiar in C: macros. It helps limiting the number of lines of code, by factoring redundant instructions. This is almost a search-and-replace. We also could have done static inline functions, but with macros so we can have functions working on either integers and floats. On the Zig side, it looks a lot like inline functions in C, except that we have a simple function with the exact same syntax as any other function, taking a type as the first argument. This type is a value known at compilation, comp-time. And it is used as the type of the other parameters. And if we want to limit the number of functions in our final binary, we could add the keyword "inline" to this function. -- comptime 2 macros for generic structures Another way to use macros, is to get generic structures. In this example, we create a list of items, in a list structure. So for each type we want, we create a structure list underscore type. Here is the declaration of a list of integers. Then we can use the list this way. In Zig, we can have a function returning the structure we want. Side note: this, isn't mandatory. So, as presented here, this feels like a very small improvement. In both versions, we still need to perform memory allocation (and handle errors) in the user code. This part still is related to the type of our structure, this doesn't feel like a good abstraction. FOR NOW. I'll show to you a real-life example later. -- comptime 3 conditional compilation Finally, we use the preprocessor for conditional compilation. Compiling only stuff we want. If _this_ is defined somewhere, then string will be "OpenBSD is awesome", otherwise if _this_ is defined, then string will be "Linux rocks". Since the switch is on a constant value which is defined once during compilation, there is no point to put other instructions than the right one in the binary. Same thing applies within functions, if you are refering to constants defined at top-level. -- test Next, a quick one. Tests! In Zig, there is a "test" keyword allowing you to execute a block of code to easily create a lot of tests for your code. This is an example. We have a function. And we have a test block, which is named. This is a block of code so you can write pretty much anything in it. And you can have as much test cases as you want directly in your original code. To run the tests, you just have to do this. If the test fails, the zig compiler will provide the name of the test and an error stack trace. That's it. Easy. Nothing much more to say. Moving on. -- errors Next, Errors. One of the best things in Zig (compared to C). First, in C, there is none. You detect errors from the integer returned by the function, each function has its convention, minus 1 won't be the same error from one function to another, and some will use NULL pointer as an error. Silly. Errors in Zig are enumeration's values. 'Error' is an enumeration of possible errors. That is exactly as an enumeration. Values (and, more importantly, return values from a function) can either be the actual value you want to return or an error. Between both types, we put an exclamation mark: actual value of this variable can be either one of these types. So you can know what gone wrong. -- errors in practice Let's see how this works in practice. In C, you'll get a value that is conventionaly an error (like minus something, or NULL). Then you'll have to deal with it according to the manual to know the different possible errors and their value. Or you can just completely ignore the return value and have a massively flawed program or library. That's fine. In Zig, function signatures tell the developers when a function is returning an error, with this exclamation mark. Return can be either void or one of these errors. If I didn't put a type of error, this would have mean any possible kind of error. Now, we call the function. Since the function may return an error, we have to catch it. In this example, our error is named 'err', and we switch on its possible values. In case the error is UnExpected, we return the error. Then, we assume that authentication error cannot happen, which is explicit with the 'unreachable' keyword. In case our assumption is wrong, and that an authentication error happens, our application will crash at runtime. Most of the time, we don't want to handle errors withing our current function. So, we have a shortcut to propagate the error, which will be managed later. This is error management is crucial to simplify the C programming language. -- error stack To finish with error management, an example. We have an application sample, with a stack of functions, with an error in the last function call. We have first our main function, then last to fail, second to fail and first to fail, which returns an error. Each time, we use the try shortcut as we saw earlier. The error is propagated. Since we don't catch and manage the error, even in the main function, this will create a runtime error. Now, we run the application. What is displayed? First, the error name. Then, the complete error stack. Each time, we have where the problem occured, the related function name and the line of code. There is no need to add any code do be able to create this error stack. And it helps a lot: we see how a program failed by default. This isn't enough, but that's great for debug. -- allocations Next, memory allocations and allocators. In Zig, an allocator is a structure with a few functions to allocate and to free memory. There are quite a few allocators, and I'll show some of them. First, we have to know how to use them. An allocator has 4 main functions, allowing you to ask for memory. You can ask for a single element with 'create', and free its memory with 'destroy'. You also can ask for multiple elements, which is translated to a slice of elements, with 'alloc', and the memory is freed with 'free'. Now, let's see some examples. The first example we'll see is 'page_allocator' which asks for a complete memory page, each time we ask for memory. A memory page is four kibibytes on Linux. In this example, we ask for three elements of a single unsigned byte. Then we 'defer' memory release at the end of the function, with the 'defer' keyword. (We will see the defer keyword in detail later, it only means doing stuff at the end of the function) Now, we can use our slice as we want. When we run this example, we can see memory allocation with strace. As I said, a whole memory page, four kibibytes, is allocated. If we change this example to ask for memory three times, we see that the application now asks for four kibibytes, three times. That is inefficient, but this is a very simple memory allocator. Let's see a more general purpose allocator. Before using it, you can configure it to check for memory safety. This way, it will - check for double free, keeping track when memory was asked, then release the first and second time, so it will provide a really useful debug message, - it will also check for memory leaks, allowing you to know if you correctly released memory, - it won't reuse memory slots, - and it works even in freestanding That's some great stuff. Now we created an instance of this structure, we say we want to deinitialize it at the end of the function, that's where it will perform verifications I just told. GeneralPurposeAllocator is an overlay on top of an allocator. It actually uses page_allocator. To use it, we now get the inner allocator, which was configured when we created the GPA structure. And now we can use it exactly as before, with 'alloc' and 'free'. GeneralPurposeAllocator isn't as simple as page_allocator, and not as stupid. When we ask for multiple memory allocations, it won't get a new memory page each time. I'll leave the details, just check the code if you want to know more. // In this example, I ask for a single small slice, // and there are 2 actual memory allocations, // which represents two buckets (for small and bigger memory allocations). // But if I ask for three small slices, there are still only two memory allocations. Next, we have 'FixedBufferAllocator', which is an allocator that doesn't allocate memory at all. It uses a fixed buffer that you provide for memory allocations. In this example, I create a buffer that is an array of a thousand unsigned bytes, filled with zeroes. I could have created this same buffer uninitialized (so, filled with garbage values). Next I create a FixedBufferAllocator structure, providing a slice of my whole array. FixedBufferAllocator have a inner allocator structure, as GeneralPurposeAllocator, so we can extract it. Then we can use it, just like before. In this case, we don't even have a memory allocation, the buffer is on the stack. That is great for performances, but stack memory have limitations. The last memory allocator I want to show is 'arena'. Arena allocator is an overlay on top of an allocator (page allocator in our case). Arena releases all allocated memory when we call its 'deinit' method. In this case, at the end of the function, all memory allocation will be released. No need to call 'destroy' or 'free', they are ignored. This may seem like a useless allocator, but it actually is great. Some applications have quite a few allocations related to each other, and may want to release all of them at once without the possiblity for a memory leak. For example, a network application, such as a webserver, may allocate memory for each request and their response (with many small allocations), and our application could free all the memory related to a request at once. Just by choosing this allocator, you are sure not to have memory leaks. And that concludes what I wanted to show about memory allocations. It is a big part of what actually is great in Zig. -- defer Last but not least: defer. The idea, just like we saw, is to perform some code before returning from the function. So you record a block of code that will be executed before the end of the function or the return statement. It is an useful, very simple and intuitive feature of Zig. Nothing new, we already saw that in Go for example, but still is just plain awesome. There is no usable feature like defer in C, well, originaly. I encountered something that tries to be defer, and a paper was published a few months ago. But that feels like an hack since you don't just have a defer statement, you have non obvious corner cases, 'guard' blocks which aren't mandatory but... Sorry INRIA laboratory of Nancy, you fought well, but your defer is no match for the defer statement in Zig. Period. So, I'll still consider that there is no defer in C. In Zig. In our first example, we start with a 'defer' block of code that'll be executed at the end of the function. And the content of the function only is a print hello world. This function will print 'hello world', then 'end of function'. In our second example, we have an errdefer statement. This means that the related expression or block of code will be executed only if our function returns an error. Next, we have a defer statement, just like before. And then we 'try' 'some function'. In case 'some function' succeeds, our 'defer' expression will be executed after that, since it is the end of the function. In case 'some function' fails, before returning its error: First, the 'defer' expression will be executed, then the 'errdefer' one, because defer and errdefer statements are queued. The last one is executed first. And now, an example of the awesomeness of defer. Let's see this example. We want to perform two memory allocations, then run two functions that may fail, and we want to end the execution uppon any failure, knowing which function failed. In C, this translates to this. Each time a function fails we have to remember to free previous allocations. There is no meaningful return. We just return a conventional negative integer with a value depending on where the error occured. None of that is necessary in Zig. Code is way simpler. A friend of mine suggested that we could have use labels and gotos. We quickly concluded that defers are better. But if you have a better way to handle this code, I'm open to suggestions. -- summary Okay, quick summary. We saw that Zig improves most C functionalities and syntax. For the new stuff, we have: optionals, simplifying the code a bit this functionality is coming in C++, but with a way less user-friendly syntax Next, test blocks. You can write a function and its test in the same file, no big deal, just nice. Next, comptime. It is a replacement for the preprocessor, allowing to use only a single language. And this helps static code analysis, and to have a better readability, amongt other things. Next, error management. They are handled with simple enumerations, handling errors is mandatory, but can be simply propagated with the 'try' keyword. We saw the code simplification. Next, allocators. They are simple structures allowing to allocate or free memory. There are many allocators, since there are many ways to manage memory. And the standard library uses provided allocator. So, you chose a way to allocate memory and the standard library will comply. Next, defer. defer keyword allows to execute code at the end of a function. errdefer is the same, but only when an error occurs. Both are dead simple, and yet, they improve code readability a lot. Finally, standard library. I didn't show much about the standard library. Let's say there are most of what we can expect from it. Common structures, such as hashes, queues, linked lists... Allocators we saw. And some operating system abstractions, so our code will work on most OS and most architectures. -- BONUS BONUS! Cross compilation works like this. So you can easily have your application compiled for Linux on an ARM archicture using the musl library. ... or for windows ... or for ARM in freestanding ... or for browsers with webassembly. And it even works for zig cc, the C compiler included with the Zig compiler. -- BONUS BONUS! Regular linker didn't work for the Apple Mach-O architecture. So... they did a new linker! Thanks to Jakub Konka And it will someday help for incremental linking, meaning that you may have an application with several million lines of code and still compile it in a second. -- Conclusion / side notes In conclusion, Zig isn't production ready just yet, they plan to have a few bug fixes, sure, but also some features that may change before 1.0. API isn't finished, by any mean, which isn't a big deal to me. The biggest drawback is that documentation is lacking. The only improvement I really want to see is a replacement for poll/epoll/kqueue/select/IOCP, such as libevent but in standard library. My opinion is, despites being minimal, changes over C have massive implications. Writing low-level code finally is fun again. Zig comes with the very few features that make it consistent and easier to write than C. What I've shown to you with comptime, defer, error management, etc. is a win to me. There are other features, but nothing really complicated. Zig developers are reluctant to add new functionalities, exactly as it should be: the fewer the better. Creating a language that works for any system is a massive amount of work, no time for half backed features. And Zig developers do pay attention to details, even if it means to create an entirely new linker. And since the Zig foundation isn't a commercial organization, I'm confident this project will thrive. Thanks for your time, thanks for watching, see you soon. I've read maybe half a book on C++, and my will to do anything with it was nowhere to be found. That may be just me. I searched for "C++ concepts", just to have a list of concepts that you may have to learn in C++. But "concepts"... is itself a concept in C++. Okay. Let's read a bit > Violations of constraints are detected at compile time Nice! > which leads to easy to follow error messages So great! Then in comments // ... 50 lines of output ... So you omit 50 lines of output to show how your error output is so great. That has to be a joke of some kind. Sorry but 50 lines of error messages, I won't read, okay? I know that's I'm cherry-picking like a bitch here, okay, but this is exactly what I expected without even searching for it.