180 comments

[ 5.8 ms ] story [ 384 ms ] thread
A short and clear introductory overview.
What is Zig's async story? Is there something like Go's coroutines?
No, it's 'code transform' async-await as is fashionable these days, but with the advantage that it's 'color blind' (the compiler will do the async transform or not based on the caller's expectation).

See: https://kristoff.it/blog/zig-colorblind-async-await/

That's really cool. I'm wondering though how Zig handles massive concurrency? Is there some possibility to have greenthreading?
Unsure what "massive concurrency means". As far as I'm aware zig offers no special primitives for threading (other than tools in a standard library to start/stop threads and such). Async/await/suspend/resume are features used for concurrency (yielding on a single thread).

You then use those primitives to implement parallelism however you want. Just like you'd do in C. Or, ykno, someone makes a library that does green threads the way you want using those primitives.

So Go has a two things that enable massive concurrency.

1. Coroutines have dynamic stacks, meaning that you can start a lot of threads without exhausting your memory: https://medium.com/a-journey-with-go/go-how-does-the-gorouti...

2. Go has a lightweight threading model that doesn't map one thread to one OS/kernel level thread. Enabling you to have thousands of threads operating at once. limited by GOMAXPROCS.

I was just wondering because the article mentioned Zig having a lot of the benefits of Rust and Go, and this is one thing where Rust doesn't shine, so I was wondering if they had an answer for this.

As said by @laserbeam there is no implementation of concurrency built-in Zig language, just primitives.

There is one event loop in the stdlib, but it has some limitations and there is some discussion to change it or remove it https://github.com/ziglang/zig/issues/8224

Some Zig libraries implementing an event loop:

- libxev by Mitchell Hashimoto https://github.com/mitchellh/libxev

- one from the Tigerbeetle DB https://github.com/tigerbeetledb/tigerbeetle/tree/main/src/i...

They both use kernel concurrency features (io_uring/epool on Linux, kqueue on MacOS). They are quite low level and do not implement a lightweight threading model comparable to Go concurrency.

Go style concurrency can be also implemented in Zig, with the caveat that since Zig doesn't have a runtime / gc, you can't expect full 1:1 mapping with what Go provides.

IMO the key point of Go's concurrency are channels, and those have already been implemented in Zig, with comparable semantics (eg blocking when the channel is full).

Here is how I use a channel (defined a few lines above) to implement the main loop of an interactive program I wrote:

https://github.com/kristoff-it/bork/blob/master/src/main.zig...

You know what you doing. Move Zig. For great justice
Is it a good alternative to Go? How is interoperability?
The Zig compiler is also a (clang compatible) C/C++/ObjC compiler, the Zig toolchain comes with C and C++ runtime headers and libraries, C headers (but not C++ or ObjC headers) can be directly imported into Zig code. Don't know how many of those checkboxes Go ticks (I think it can at least import C headers?).

Whether it's going to become a good alternative for Go will depend mainly on the stdlib I think. What's clear is that the Zig stdlib will be much richer than C's or C++'s, but whether it will be a good match for Go's stdlib for backend work remains to be seen.

I expect the stdlib will not be quite as tailored for general backend work as Go’s, but I envisage we’ll ultimately see a rich and vibrant package ecosystem with all those bases covered.
> Don't know how many of those checkboxes Go ticks (I think it can at least import C headers?).

Go lets you write C in your Go files, and reference it with a psuedo-package, "C". So you can import C headers, but Go itself doesn't come with a C compiler toolchain, and ends up relying on your system's C compiler, and thus you lose Go's cross-compilation, unless you have the headers for your target platform. I've seen some have been using "zig cc" as their C compiler with Go, given it makes cross-compilation easy.

It is not an alternative to Go same as C++ or Rust is not really an alternative to Go. They are on a different level, but can do the same job. Though I could argue that Tcl could be an alternative to Go, I think many would not agree.

Zig has manual memory management, doesn't have much of a runtime (no CSP out of the box) and above all is not mature yet (breaking changes do happen). As of now it has one of the best stories for interfacing with C as it can directly use C headers.

I would be interested in hearing more about your point.

TCL is an elegant scripting language, very LISP-y, where most data structures are lists of strings.

Go on the other hand is more like a modern mix of C and Pascal with GC and lots of convenient libraries, and which builds statically.

The performance of TCL and Go are also a world apart.

Go is a worse Java 1.1. The only remotely interesting part is virtual threads.
Might as well use Java 19 then. Why not, really. Because Java is boring?
I am pulling a leg here a bit. I could risk a statement that Tcl could be an alternative when all you want is a simple CRUD backend. Now performance considerations are valid, but in some applications Tcl could be fast enough. Both languages, or maybe we could say platforms, have manged runtimes and maybe that's were similarities end.
Tcl is good for both CRUD and REST; for example, the sqlite.org web site uses a pure Tcl web server, through which you can access sqlite's SCM app which is backed by a SQLite database.
I'm hearing this so often that I think I have to write a blog post some day. I think both Zig and Rust can be an alternative to Go. Zig is still young, so it probably lacks tools for many domains, but if it grows I expect it to get more tooling similarly to what's happening for Rust. So just talking for Rust today, it's used in:

- gaming

- web apps

- networking

- CLIs

- distributed systems

- crypto

- systems programming (like for example databases)

- language tooling (for example JS ecosystem has a lot of Rust based tools being developed right now)

and probably much more.

Go is used for every single of these categories too. Databases? Check. Networking? Check. CLIs? Check. And so on, and so on. The truth is that it's often a matter of taste, timing and various constraints that are often non technical at all.

Taste is the most subjective reason, nevertheless it's often a driving choice. If I start a new personal project I usually default to Rust these days, unless it's a bad fit, but I've successfully used Rust in many different domains, so it's safe to say Rust covers most of my programming needs (other than maybe a quick throwaway script). Even if Go was a sligtly better fit, I would choose Rust anyway, cause I just enjoy it more.

When it comes to constraints, I think that is most often relevant in a professional setting. I've worked in many different companies and very rarely, especially when a company is on the bigger side, you can just choose whatever tools you want just based on technical requirements. If a company is a Ruby shop, they will have to have a very strong reason to start a new project in a different language, even if it is clearly a better choice. But then a good question is: what makes a tool a better choice? It will very often be really hard to tell until you actually do the project. Have you ever been in a discussion about choosing a new language for a project? I have been, lots of times. One time was especially interesting, cause we were discussing what to use for a real time application keeping a long running websocket connections. We were considering Ruby (it was a Ruby shop), Go, Elixir and Rust. We chose Rust in the end, but I'm sure that we could have been successful with Elixir or Go too. The most interesting part? There was an experienced developer that was pushing for Ruby hard. He was working as a "Principal Engineer" and if it was up to him I'm sure he would have chosen Ruby, which for me was the least feasible choice. Would it be a good choice? I don't think so, but I can't say for certain and we will probably never know. So as you can see, even with very clear requirements people will disagree on "the best tool for the job" (which is also why I hate this term). But even if everyone agrees on a technical aspect, there might be also other constraints. If a company is growing very fast they might be worried about hiring for example. And they may choose a language that is technically a bit worse than some alternatives, but has a bigger talent pool.

Timing is a tricky one, cause at the moment both languages are quite mature, but if you think about it, a lot of well known big projects in Go (k8s, docker, terraform etc) were started when Rust was honestly quite rough. So while I'm not saying if k8s was started today, it would be definitely started in Rust, I think that at least some of those projects might consider Rust. The same goes for companies that are invested in Go already. Even if Rust made sense for some of the new projects they would probably lean heavily towards Go, cause of a collective company experience.

So, yes, Rust is an alternative to Go in many (most?) domains. In the same way Zig can be in this place too when it gets more tooling and maturity.

In a way I feel that everything is an alternative to everything and at the same time nothing is.

As you said it all depends on the circumstances. But I don't really see Zig competing with Go. They both can do mostly the same things, but they both approach them from quite a different sides.

For example bash is being used in:

- gaming (https://github.com/JosefZIla/bash2048)

- web apps (https://github.com/avleen/bashttpd)

- networking

- CLIs

- distributed systems (https://github.com/frameable/aviary.sh)

- crypto (https://armedia.com/blog/blockchain-program-written-bash/ https://github.com/grondilu/bitcoin-bash-tools)

- systems programming (https://github.com/damphat/kv-bash)

- language tooling

Some of those make more sense than others. However we all talk about a mythical general case. For every language there are niches that are covered by it more significantly. For Go it would probably be web backend. It doesn't mean it is only suited to this one niche, it is used in everything. In general it is used there more. I don't believe that Rust sees the most use in the same niche to the same order that Go sees it.

Is Rust or Zig an alternative to php, awk or Lisp or vice versa? In practice I don't really think so.

I guess it all depends on one's definition of "alternative". I don't think that a statistical Go programmer would see Zig as a real alternative. Statistical C programmer might see it as a Go alternative, but that probably would not be a question he would ask.

Coming from a Go background I found it quite easy to use Zig, as the language is small and you can be proficient in few days.

Of course you have more to handle by yourself in Zig. For example a simple string concatenation will need more work as there is no implicit heap allocation.

For interoperability with Go it's quite the same than C<>Go through Cgo: doable but not ideal.

However the Zig toolchain can be used to improve the build system of Go projects with Cgo dependancies. That's what Uber is doing https://jakstys.lt/2022/how-uber-uses-zig/

A quote from the article:

> A distinctive feature of Zig is that it does not deal with memory allocation directly in the language. There is no malloc keyword like in C/C++. Instead, access to the heap is handled explicitly in the standard library.

I guess it is all you need to know about the article quality.

The point that paragraph is trying to make but articulates imperfectly is that you explicitly pass an Allocator to the stdlib routines that allocate -- rather than having them allocate on the heap implicitly.
Yes, but it still is a part of the standard library and how the standard library does things - a convention. Same as in C, as malloc is just a function. You could create a replacement library for C stdlib and it could also expect an allocator in the parameters if a given function could allocate.
You could, but it's not idiomatic in C. Zig is basically built around this.
It would be hard for a replacement with the purpose of modernizing a way of working to be idiomatic at the same time. It would be an explicit attempt to update what is idiomatic, and of course not be the same as what was before.

It's actually a rather interesting idea, if I had the time for a C-focused side project that sounds really fun! :)

Their standard libraries are built around the notion, but I would argue that putting allocation in their respective stdlibs instead of a part of the language makes it possible to be flexible if needed. There are non-trivial and semi-popular C programs that do not use the C stdlib at all.

I agree that the culture is different and that is significant, but I do not see it in the notion of the language itself (without its stdlib).

Also what is idiomatic for Zig does change as it is not a mature language. Though the allocator passing is expected to remain. Odin did an interesting thing and the allocator is put in a 'context' which is an actual part of the language.

>Yes, but it still is a part of the standard library and how the standard library does things - a convention. Same as in C, as malloc is just a function.

Yes, but it doesn't argue that it's not the same in C. Just that it's not the same as languages who do the memory allocation themselves.

>You could create a replacement library for C stdlib and it could also expect an allocator in the parameters if a given function could allocate.

You could do anyting in C, even write Zig in it. And vice versa, you co do malloc in Zig. But allocators are how Zig is designed/used, whereas C opts for malloc.

So you have coloured functions? The red that can allocate, and the blue that cannot?
It's not quite as clear-cut, some stdlib modules take an allocator when an 'object' is created (containers mostly work like this), others on each function call that needs to allocate (but crucially, one way or another, an allocator needs to be passed in).

In any case it's not nearly as limiting as the 'red-blue' async-await color split in other languages, and I can't think of a single downside of explicitly passing allocators into stdlib modules instead of having a hidden global allocator.

The approach to always pass allocators isn't all that Zig specific either, it's also a good library design practice in pretty much all other languages which allow explicit memory management.

Well, you have expicit allocators. A popular branch of the field calls this ‘dependency injection’. It's true that the current standard library never allocates implicitly, because that would be bad on constrained projects, but there's nothing stopping you from writing code that does so, or presumably even using libc.
If you want to think of it that way, the stdlib `voluntarily` colors its functions by allocator. You can do whatever you want in your own functions, including always using a global allocator and never passing it anywhere.
No, I wouldn't describe it like that. It's more that there isn't an ambient, implicit Allocator lying around for stdlib to use. It is explicitly provided by callers.
(comment deleted)
After using zig for a few projects, it's amazing how it feels similar to C when coding.
The feeling to micromanage every details instead of focusing on the actual business logic?
when those details are critical to your business, yea
Isn’t that the whole point of a low-level language? No sane person would be writing CRUD apps in Zig, the same way no one does it in C.

Your video card driver on the other hand doesn’t have too much “business logic”, but really cares about all those pesky details.

I don’t know why these new languages have to have their own special takes on loops without giving us the classic C99 version of the three-clause-for-loop. Even JavaScript has it. If they want to improve on it, then allow us to declare variables of different types in the first clause. Yes, using the C-style loop to iterate over a container sucks, but that is an argument for also having a for-each loop. There are lots of low level algorithms that are just better to express with the three-clause-for-loop.
C-style for loops can almost always be replaced with iterators in a more concise manner. No need for the explicit termination check or the explicit advancement expression, that's already handled by the iterator.

  > C-style for loops can almost always be replaced with iterators in a more concise manner.
Not if you want to optimise for performance.

A simple example: I search through a list of n items to find item m, iterators typically don't let you break out of the iteration.

Zig is the only contender I've seen where the for loop looks iterator-like, but also supports break/continue.

> A simple example: I search through a list of n items to find item m, iterators typically don't let you break out of the iteration.

That'd be a pretty crappy iterator implementation, short-circuiting is basically what iterators are all about (how else would you handle infinite iterators?). For example, in Rust, it's simply Iterator::find(): https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...

for loops in Rust are basically just syntax sugar for a while loop taking an element of the iterator each iteration, you can use break/continue as normal.

Some languages support early return(break) and labeled return (continue) in lambdas. Although good languages have lazy iterators or optimizing compilers. One would use iterator.takeWhile(x => x is not m) in your case, or filter, and skip in other cases. Better languages even allow you to yield return and write your own lazy iterator transformations.
Is there any language that won’t allow breaking out from an iteration? I can only imagine some FL language where the default loop construct would disallow that, but even there it is quite common to just return early syntactic sugar.
> iterators typically don't let you break out of the iteration. Zig is the only contender I've seen where the for loop looks iterator-like, but also supports break/continue.

I'm curious as to which language you worked with that didn't allow this.

Dlang isn't a contender here? It can do most everything Zig can do, speaks C natively, and it supports break/continue?

edit: plus has a GC if you don't want to do manual memory management, already has a package manager, good at producing native/static binaries, can do automatic type inference, user defined types, cross compiling is super easy...

You made a good point, that is often overlooked about memory management, as if all users will always want to do it manually. The reality is, many don't want to deal with it at all or only for specific projects and reasons. For new and various users, not having to worry about manual memory management until they are ready or want to, can be a huge relief or increase productivity.
Iterators are often nice, but are no replacement for loops. Try processing multidimensional arrays without loops.
> iterators

> without loops

This is a false dichotomy. You can use iterators to replace three-clause loops without changing the body:

    for row in 0..rows {
      for col in 0..cols {
        arr[row][col]
      }
    }
This is far easier to read than three-clause loops which repeat redundant information repeatedly and redundantly.
OK, but you're still using loops.
Because the classic for loop is prone to off-by-one errors, and it's unnecessarily boilerplatey for simple cases. OTOH complex cases that take advantage of customizing the three clauses or independently modify iteration variable(s) are usually "clever", not in a good way.
You can still have it but add more modern syntax for looping. Hell Java has "goto" reserved if I remember correctly.
All languages with a simplified `for` loop have kept `while` for the other cases. That makes the old `for` just a redundant syntax.
Zig supports the 2nd and 3rd clause already in its while loop. Would it kill it to accept an initializer and look more like other languages instead of having a special snowflake syntax.

Is this (note the enclosing braces)

  {
    var thing = thinger();
    while(cond(thing)) : (mutate(thing)){
      // do whatever
    }
  }
really better than this?

  for(var thing = thinger(); cond(thing); mutate(thing)){
    // do whatever
  }
Too many dots. It feels like programming in a OOP language, which C isn't.
UFCS is hardly "OOP", it's just a bit of syntax sugar to allow function call chaining instead of nesting.
But isn't function call chaining generally an antipattern? And you can do it without Universal Function Call Syntax anyway, and besides... it's ugly as hell.

  # Writing it like this is bad:
  print (length (drop_failed (get_students (school_system))));
  # But like this is okay?
  school_system.get_students().drop_failed().length().print();
Personally I find the chaining a lot more readable, but maybe that's just me :)
It's not just you. Long function chains/nested function calls are discouraged in most languages for readability reasons, but chaining is 100% more readable to everyone who has tried both for any appreciable length of time.
Chaining this much is not really recommended in Zig. An idiomatic implementation of that stuff would not allow you to chain this much.

Additionally, in Zig temporaries (which you would be implicitly constructing by chaining) are immutable, meaning that a chain of calls would only compile as long as it doesn't try to mutate any intermediate value (which is a nice protection against footguns).

But really, this is a coding style that's considered fine in Rust, not in Zig.

So, instead of doing functions like every other modern language in history, you intentonally introduce foreign syntax?

And still compare it to C?

That article is about control flow and syntax, which isn't a big problem. What matters is data. So what does Zig have that C doesn't?

- Arrays and slices. A slice is a pointer and a length. There's subscript checking on slices. You'd expect that the preferred operation would be to take a slice from a slice, but the documentation does not mention that option. This may be a documentation error. There is a ".." operator for specifying bounds within a slice, but the examples given only show it being used to extract a fixed-length array. Slices are important. They let you do most of the things done with pointer arithmetic in C, usually badly.

- No objects, just structs. Structs are available in "packed" mode (no filler bits), something rarely seen since Pascal. There are discriminated variant types (Rust calls those enums) and undiscriminated variants (like C unions.)

- Strings are unchecked UTF-8, which is probably OK.

- "Sentinel terminated arrays" are a generalization of null-terminated strings. Strange, but perhaps harmless. The "char *argv[]" array in C main programs is a null-terminated array of null-terminated strings. You don't really need "argc". That's one of the last remnants of that idea, and it doesn't seem worth reviving.

It's reasonable enough, but it's maybe the fifth try at a better C.

[1] https://ziglang.org/documentation/0.10.1/#Slices

> No objects, just structs

C doesn't have 'objects' either, just structs.

In Zig you can add functions to structs though, and you have UFCS syntax sugar, both together is pretty close to class methods ;)

Those sentinel terminated arrays are a killer feature for C API interop, since you don't need to allocate separate C strings on the stack or heap just for passing string literals into the C API (Zig string literals are such sentinel-terminated arrays, so they are both slices and zero-terminated for C string compatibility). I really wish Rust would use the same idea, it would simplify creating C API wrappers a lot.

One of my favourite features of Zig is actually the arbitrary width integers, C23 is getting those too though.

...and in general Zig is fixing all the 'sloppy parts' of C, for instance it's very rigorous about not allowing implicit conversions which would lose data (e.g. trying to assign an u8 to an i8 is an error, u7 to i8 is allowed though, because no data would be lost).

PS: also how could I forget about the comptime features, this is for instance an area which is ignored by many other attempts of creating a better C, but one of the core features of Zig.

I think one of the author's problems is that he conflates C and C++ and it seems they do not have a knowledge around either. This quote also supports this:

> There is no malloc keyword like in C/C++.

Are you nitpicking about it being a keyword in C,

https://en.cppreference.com/w/c/memory/malloc

and a standard library function in C++

https://en.cppreference.com/w/cpp/memory/c/malloc

?

That's not a keyword. It's just a free function, maybe you could argue it is an intrinsic since it's magic and needs to some extent to be blessed by the implementation

(It is conjuring things from "nowhere" in the higher level language, obviously normal functions aren't supposed to just make objects, only transform them - but this can, so that's magic)

Ah yes, you're right, it's not strictly speaking a keyword in either one.
It is not a keyword in C, so it is not a nit. For all the language is concerned it is just a function that takes a number and returns a pointer. If a compiler treats it any different it is only a question of optimizations or to generate helpful warning/error messages.

You can point linker to a different implementation for all it cares.

Technically C has objects. That's any chunk of memory with a type, size, alignment and duration. Incidentally C++ has exactly the same definition.

Just being pedantic, nothing to see here.

C++ has syntax for adding functions to objects and type system support for deriving object types from other object types and overriding those member functions. There are some who consider these a rather important thing.
I'm just saying that, pedantically, "object" is a term of art in C and C++ which has little to do with the typical OOP definition of object.
Indeed. For example the language needs a notion of these objects having some particular type, and they need to be able to have lifetimes, so that we can talk about ideas like type punning (legal in C in some ways, not legal in C++), about aliasing rules, and about provenance.

If you don't want to do any of this, you can't achieve the performance from high level software (where "high level" here means C rather than machine code) that people have come to expect and demand.

These are pretty much the same thing, though. The C-specific definition of "object" was devised to make C usable with hardware architectures like segmented memory, which was being marketed as "OOP down to the hardware" back in the 1980s. The fact that the term "object" is used here is not a coincidence.
Just out of curiosity, what is the typical OOP definition of object?
Ah, that's a trick question. There is no typical OOP definition of object. I used to think that only aspect everybody agrees on is late binding, but some people consider traits-like static binding a form of OOP.
Well, you brought up the subject :) I think it's a bit intellectually dishonest to appeal to typical OOP definition when there isn't one (except that your language isn't true OOP but my language, I won't tell which one, is). And there are quite a few different definitions of late binding too.
(comment deleted)
There are multiple contradicting definitions of OOP, but I don't know any that would include C POD types and primitives.
The original claim was that C++ "objects" and C PODs and primitives are the same. It's a rather odd claim, and the questions whether C++ "objects" are true objects, there is even a definition of true object, or was object oriented programming a good idea to begin with, don't seem to be closely related.
The standard definition definition of Object in C++ is the same as the definition of Object in C because the standard says so: https://en.cppreference.com/w/cpp/language/object .

Is not a problem of interpretation. It is just that the C standard uses the term object for just a bunch of memory (with a type) and the C++ standard does so too. Nothing to do with objects in the OOP sense.

This is interesting. The page you linked mentions that objects can be created using operator new, but I was not aware that C has such thing. Further down polymorphic objects are introduced, and I am fairly sure C does not have those. My favorite detail is mention of "implicitly defined copy/move special member functions", but it is never explained what member functions are. Almost as if you were using a definition that is missing something to prove some kind of point.

By the way, the document you linked to is not the standard. The published standards are not freely available, but here is the relevant part of current draft: https://eel.is/c++draft/intro.object

> That's any chunk of memory with a type, size, alignment and duration.

Isn't that just "a variable"?

  int * int_ptr = malloc(sizeof(int));
int_ptr is a variable (and also an object), the pointed to memory is an object but not a variable.
I think the point is that the C standard (or K&R, or both? I forget) repeatedly refers to "objects", and uses the word differently than we're used to in OOP
> I really wish Rust would use the same idea, it would simplify creating C API wrappers a lot.

Zero terminated strings suck, you need to do allocation everywhere to work with them, or else you must in-place mutate the strings in hard to reason about ways and they can't fully represent arbitrary text. So I definitely wouldn't want Rust's &str string references to have either behaviour.

If what you do is interact closely with C wrappers, Rust of course provides a type for this: std::ffi::CString and that's fine, but it is a miserable structure to use for actual string work.

Well yeah, zero terminated strings suck, but that doesn't change the fact that pretty much all operating systems and fundamental C libraries expect strings to be zero terminated, it's not just a C quirk anymore, but burned forever into operating system ABIs too.

A programming language which is supposed to interact with those APIs directly really shouldn't make this harder than it needs to be, and Zig's sentinel-terminated arrays are a pretty clever solution to the problem (even though they still need a temporary allocation to convert a regular slice to a zero-terminated slice for "dynamically built strings", but one can get surprisingly far with just string literals).

And Rust has this for when you really hate yourself enough to use it. CString and CStr
> pretty much all operating systems and fundamental C libraries expect strings to be zero terminated

C libraries are going to mostly be like that because it's how C works. If that's our high bar we're never getting anywhere.

Operating systems don't use many strings -- strings are awful because they're variable size and need parsing. Filenames are the most obvious place the kernel needs strings, and here the 0 terminating string is pretty horrible actually, the kernel can't assume the sentinel is present as that's obviously a massive security hole, so we end up not benefiting from the use of the sentinel, it's there because of C compatibility, as that wanes in significance there's no reason it needs to remain.

On most operating systems the kernel system call ABI is not stable, which means you aren't supposed to rely on this stuff anyway, and once you're not talking directly to the kernel ABI you're free to choose abstractions which better suit both sides.

GP made a great point and I get the impression you're fighting really hard to ignore reality. Idealistic thinking doesn't win the practical exams. Zero-terminated strings are a fact of life and essential to using many useful APIs. Like it or not, if a new ecosystem wants adoption it had better interoperate with the established ones without much friction.

Apart from that, zero-terminated strings aren't strictly bad. They offer an in-band signal that is sometimes very practical. Slices are more generally flexible, especially w.r.t substrings, but when you look at a memory dump for example it's great to have those terminators.

> once you're not talking directly to the kernel ABI you're free to choose abstractions which better suit both sides.

Requiring string copies just to reshape arguments according to idealistic beliefs is a huge yak shave. It means creating all those wrappers, handling all those extra failure modes, and coming up with extra versions of records (often on-disk structures) that have fixed size zero-terminated string buffers embedded.

I've been on both sides of this fence, decades writing C before I wrote any Rust - and no, IMNSHO the Yak barbering necessary is much more extensive and annoying with zero terminated strings.

The on-disk fixed size structure stuff is actually nicer in a language that favours slices, because with C-strings we're incurring a special case, what happens when the sub-structure is full? With slices that's just fine, but with zero termination that's not a valid string (because the sentinel is missing) and so you have to decide what to do about that.

> so you have to decide what to do about that

No, you need to do what needs to be done according to the on-disk format. The disk doesn't care what you think is the ideal string representation. If the available space is used up, it is used up. That's just the semantics that come with the physical reality of existing systems where you can't just malloc an extra space on the heap. Having an internal representation that matches what's on the disk is the right call, everything else is only layering complexity.

Also, you're making it look like it was hard to use, but those semantics are very easy to program against, using something like snprintf(buffer, sizeof buffer, "%s", ...), or just do it manually.

Maybe the problem you've got is that you actually haven't understood what the alternative you don't like even is in practice ?

  // Assuming buffer is a mut [u8] and text is the text value we'd like to write into it
  // if we've got some string, just let text = string.as_bytes();

  if text.len() > buffer.len() {
    // As with snprintf we need to decide what to do if it won't all fit
    // ... for example let's just truncate it
    let truncated = &text[..buffer.len()];
    buffer.copy_from_slice(truncated);
  } else {
    let (left, _) = buffer.split_at_mut(text.len());
    left.copy_from_slice(&text);
  }
As I thought I'd explained, the yak shaving problems for your C-style strings are on the other side, reading this structure. With pointer + length strings this is no trouble but with a 0-terminated string you have to conjure that sentinel from somewhere, maybe allocating in the process. Slower and uglier.
Sometimes what's most important is not what you have but what you don't have. Zig doesn't have preprocessor macros; it doesn't have dangerous C-like unions (unless explicitly requested via "unsafe" constructs). Pointer arithmetic and casts are explicitly delineated as unsafe constructs, and are more easily avoided. What this means is that (unless the clearly demarcated "unsafe" constructs are used) all pointers in a Zig program are known, and cannot be created "from nothingness" (as in C).

In addition, Zig, while about as simple as C, has the same expressiveness as C++, not to mention its exceptional build capabilities.

I don't know if all that would prove sufficient to one day replace C and/or C++ -- replacing incumbents is always an uphill battle -- but Zig is the most revolutionary (rather than evolutionary) low-level language we've seen in a very, very long time. It offers a completely different approach to how low-level programs are to be written and built.

Because Zig is so simple, how revolutionary it is can be missed when you look at one feature at a time, and can only be appreciated once you consider the whole: The mix of simplicity and power, and the eye toward tooling.

I don't think that unsafe constructs are clearly delineated in Zig. @intToPtr and @ptrToInt are in the same namespace as @max. The raw pointer of a slice is a member you can access with mySlice.ptr. A user came into the discord a month or so ago and asked a question about a program they had written where they compared a value to undefined. This is obviously never correct and they meant to make the variable nullable and compare it to null.
> I don't think that unsafe constructs are clearly delineated in Zig.

That's the real problem. It's a mixture of safe and unsafe features, with no clear boundary. C++ is like that. The trouble is, people keep using raw pointers and mess up. Read CERT advisories. Most of them come from that class of error.

This is the same problem all the attempts to fix up C have hit.

Just say no to unsafe code.

The boundary in Zig is clear, certainly clearer than in C or C++. Unsafe pointers that allow pointer arithmetic have a special type and are far less common than in C++ (and obviously C). Other unsafe operations are clearly grepped, which is not at all the case in C and C++. It is also a much simpler language than C++, which may offers easier understanding and review.

The problem with eliminating unsafe code altogether is that doing so has a cost that, in some situations, is not worth it. Sometimes it's a runtime code in memory footprint or CPU resources; sometimes it's a cost in code complexity or development speed, which may have an adverse impact on correctness, which is the real thing we're interested in.

Finding the sweet spots among the various options can only be done empirically, and because Zig is so different (it's as different from C as it is from C++ and as different from either as it is from Rust) there's nothing we can extrapolate from. Because it is its very particular combination of features (and lack thereof) that is its message, those who look at a particular aspect and say, oh, this feature is like language X, therefore Zig will have the characteristics of X, completely miss what Zig does. It tries to strike a particular balance between the compiler, the human reader, and the tooling and development experience that can only be considered as a whole.

I have no idea if Zig is "good" or if it will be successful -- only time will tell -- but I find the language so refreshing and fascinating because it is a radical departure from everything we've seen in that space in quite a long time. There is no language with a similar mix of features (and lack of features).

I fear that it will fail because it rejects information hiding. There isn't all that much new C being written, so it seems counterproductive to renounce the users that (have to) follow coding standards like MISRA C and CERT C.
> information hiding

An interesting point, especially at API boundaries.

C and Zig not having information hiding is to some extent a feature. C++ is notorious for having information hiding without safety. Almost no other language has that combination.

I certainly understand the Zig core's arguments, but I don't think they'll matter in the places where these C coding standards are used. I'm sure an external linter could solve the problem using some naming convention or magic comments, but I'm not sure it'll get far enough for that to be written. [Edit: there does seem to be community interest in metadata annotations for external static analysis https://github.com/ziglang/zig/issues/14656 ]

Although I only have a beginner knowledge of the language, it seems to me that Zig is just on the edge of being able to provide a nice unification of the C opaque-pointer idiom with other opaque handles (e.g. POSIX file descriptors, GL handles).

I can't say that I feel naturally attracted to Zig's position on information hiding (although I do find the whole package a good fit for most of my personal preferences), but it's hard for me to predict how the gestalt of its features will perform in the market, not to mention that the market success of any product is affected by many factors that aren't intrinsic to the product.

BTW, how did you reach the conclusion there isn't "all that much new C being written"?

‘Much’ is a relative term; there may be more absolute lines being written today than at its relative peak. But I see the spaces where a green-field project would choose C shrinking; even lightbulbs run C++ now that economies of scale have made 32-bit controllers so cheap. (I've written a lot of C, including parts of a C compiler, so I know it well, and I'm just not seeing it get much attention now. But if I could write Zig in a job I do think that would be more fun than writing C in a job again.)
This is exactly why I'm excited about Zig. It offers everything that was powerful and fun about C, and much more (comptime, error handling, better types, etc.), but without the warts that made me avoid C during the last 20 years.
It's not only low level. I think it can compete with Go in many areas.
> That article is about control flow and syntax, which isn't a big problem. What matters is data.

I fully agree that what matters is data but I stumbled over your remark that control flow and syntax mightn't be an issue. In fact I'd go so far as to say that above a certain level of abstraction it's all about control flow and syntax. But then Zig doesn't seem to be addressing such levels so perhaps you're right.

Of all the new languages I find zig most interesting because it claims to not need build systems (not sure if I have that right?), it focuses on c library compatibility, and most importantly it aims to be a small language.

I’d like to love rust but it’s so big I just don’t have time to learn it all. It’s a pity rust is so damn big. Anyhow zig looks like a great alternative even if it lacks the safety of rust.

> because it claims to not need build systems

Zig has a build system, but it's integrated into the compiler and stdlib (e.g. there's usually a 'build.zig' file in the project root which is regular Zig code using 'build system' modules from stdlib, this build.zig is then transparently compiled and run to 'perform the build').

(it's interesting that nothing of this is so special to Zig that other compilers - even C compilers - couldn't use the same approach)

Honestly, the lack of separation of concerns between compiling, linking, building, and packaging is what gives me pause about zig. I don't see it having a nice onramp for my existing C and C++ codebases.

It's trying to make a new systems programming ecosystem, sort of, but also trying to interop well with C source code but not at the tooling level... not exactly a coherent adoption curve.

Is this onramp problem an actual issue you've run into or a speculative issue that you anticipate but haven't actually tried?
I don't have a repo that demonstrates now, no. But it's dismissive to call it speculative. I've seen this sort of problem in smaller situations in the past, like projects whose build systems assume toolchains that aren't what are used in my packaging ecosystem. Replacing all my compilers and linkers and build systems is at least an order of magnitude more disruptive than that.
(comment deleted)
I have no dog in this race as I don't use Zig at the moment, but I'm not sure speculative is that unfair. Zig can literally be a drop-in replacement for GCC/clang [1], meaning you don't even have to fully adopt Zig as the build tool, so your initial comment about a lack of separation of concerns doesn't seem correct: just use "zig cc" for the C/C++ source if that's all you want.

That said, using Zig as the build tool seems to bring trivial cross-compilation support and portability, without having to deal with linkers [2]. Cross-compilation has always been and remains a PITA, so that's pretty compelling.

[1] https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...

[2] https://zig.news/kristoff/compile-a-c-c-project-with-zig-368...

In practice it works very well though, I experimented a bit with replacing cmake with build.zig for a 'not-quite-trivial' C++ project, and tbh for cross-platform code that's a lot nicer than wrestling with the arcane cmake syntax and all the C/C++ compiler toolchain differences:

https://github.com/floooh/sokol-tools/blob/master/build.zig

The basic idea is the same as in all other build systems: you describe your build as number of build steps and their dependencies. There's really no reason why this build description should happen in a separate build tool and language.

Right. But switching build systems is a big ask for a lot of situations. The build workflow is part of the API for a lot users of that project.
Just because it’s the default doesn’t mean it’s the only option.

Another approach would be to integrate your zig code as a static library, and have your existing build system call out to zig’s build system just for that part of the build. Linking it to the rest of your code could then be performed by your existing build system, just as it would link any other library.

In practice, Zig is quite flexible.

I'd like that plan a lot more if there were any normal way for build systems to communicate about build requirements and outcomes and the zig toolchain implemented it.
That’s as much make’s fault as Zig. You can go further and eschew the zig build system, using `zig build-lib`, I’d that helps.
Does Zig still have that insane compiler enforced no unused variables policy? Honestly, that is such a stupid thing that it literally kept me away from the language, which I otherwise find interesting for its niche.
The feature is really quite ok when working in an IDE where those errors are displayed immediately as error squiggles, and there's now also an error for any 'left-over'

    _ = unused_variable;
...statements (although I'm starting to prefer Typescript's convention to mark unused variables and function parameters with a leading underscore to suppress unused errors).

In any case, once I started to write more TS code where the TS compiler and linters are usually configured to also catch any unused variables as "errors" I no longer see Zig's stance on the same thing as critical as before, it's more or less "just" a tooling issue.

But why should I have to recursively modify my code when I am experimenting with shit? Like, am I the one that does something wrong, because commenting/uncommenting a single line to check the outcome of a program is extremely common in my workflow, and that fkin compile error can absolutely throw me out of the loop of what I was trying to do.

And it is not even hard to solve, just add a “production” profile where it is an error for all I care, and a “debug” one where it really shouldn’t be linting my code unnecessarily.

To be clear, I would also actually prefer if those were warnings in development mode, but so far at least Zig has no warnings, only errors. But I'm open to the idea that the Zig team knows what they are doing ;)
AIUI Zig has embraced the view that either something is bad, and so it's an error and we shouldn't compile, or it's fine so no need for a warning. I don't think this actually is a reasonable stance although I can see why C might make you think so.

Rust's linter, clippy, provides a rich seam of stuff that definitely shouldn't be fatal but some / many people would value knowing about. For example yesterday I built a Range which intentionally is the wrong way up, isize::MAX..isize::MIN - and so clippy says well, that's probably not what you wanted. This is a good lint. If I ever do the same thing by mistake I definitely want to know about it, but this time was not a mistake, so I wrote an allow attribute on that Range to silence the linter and also flag to readers, "No, this is on purpose".

Really seems like something that should be a lint rule..
(comment deleted)
Product of the creator's will. Andrew is confident that always rejecting programs with unused variables catches bugs, and he believes that if you don't like this you should have software to adapt the Zig you write so that it compiles, by adding or removing _ = unused_variable; style statements to consume the otherwise unused variables.

There can be a benefit to a singular coherent vision behind something - committees can produce languages which don't do anything well by compromising everywhere - but one of the negatives of visionaries is that they can stubbornly stick to their preferences overriding any other consideration. Andrew is quite sure he's right, nothing will overcome that.

It's OK, C++ manages to suffer from afflictions of a single coherent vision (Bjarne Stroustrup's) and having an unwieldy committee structure (WG21 the "C++ Standards Committee") and despite the resulting flaws it was enormously successful.

What's an example of a bug that would be caused by having an unused variable?

I honestly can't think of one.

Store an error code forget to check it, store a future but fail to poll it, etc.
Which is absolutely not solved by assigning it to _ neither, if anything it will just make the variable appear used in syntax highlighting and that will make me forget to properly use it!

IDEs just gray out the unused variables and that is an 1000x better way of handling this issue.

I think you misunderstand the purpose of assigning to _.

Extremely common bug 1: a function returns an error/future, but you don’t check/poll it “foo();”. May happen because you copy some tutorial code that’s not rigid about error checking, or you don’t realize that this language doesn’t have futures that run without being polled.

Quite common bug 2: you store the error/future, but don’t check/poll it “let a = foo();”. May happen when moving code, certain branches, copy paste error, not enough coffee.

Opt-in: it doesn’t matter if the call succeeded or the future is polled, leave me alone “let _ = foo()”. May happen in test code for example. This is not the common case, so the opt-in annoyance is justified to improve the common case.

You misunderstand me - I’m talking about my original problem of an unused variable that happened due to me commenting out something for example. Temporarily unused variable if you will.

Without recursively commenting out any further variable that have also become unused by my action (which I hope we can all agree is extreme tedious and error prone), I am left with no choice but assigning it to _, which as you mentioned already have a normal usage, making it later very hard to discern which is just a temporarily unused variable, or a deliberately ignored one. The very “feature” can cause bugs, besides being annoying as hell.

I agree. I gave Zig a fair shake, even went so far as to find the compiler PR that automatically adds `_ = foo` to your source code while compiling[1] and set up the VSCode extension with autofix. I couldn't get used to seeing the lines with `_ =` appear and disappear all over my code while I was typing.

With the usual warnings approach, you can rely on compiler/IDE/pre-commit tooling to find unused variables - they'll all nag you until it's fixed. With Zig's autofix approach, those problems are immediately silenced and you don't have any help from the compiler or tooling to find unused variables. It's quite an ironic outcome if you think about it.

[1]: https://github.com/ziglang/zig/pull/12803

That is something a decent type system should solve - make it impossible to pass 'incomplete values' on, so any state further on which depends on the error handled/not handled will expect the appropriate type and compiler will error at that call site.

An unused variable means you weren't passing it on anywhere so there is no code which depends on its value, so how can it be a bug?

future = x.do_async(); return;

should not error out because of 'unused variable', it should give a error message concerning the lifetime of the future object.

using the wrong variable in repeated code:

value1 <- f()

value2 <- f()

g(value1)

g(value1)

or other similar cases

As someone who works at a company with an old horrible code base, for me its not so much of it can cause bugs, but it presents people from doing stupid things. In parts of a codebase we have a long legacy function that contains a string that tries to show what "state" a process is in, but the string is only ever used for that. Never is it actually used by the system. So we have code that looks like this

  public void processTransaction() {
    string state = "start";
    getCardDetails();
    state = "serialize";
    serializeCardData();
    state = "send data";
    sendData();
    state = "check return code";
    bool success = checkReturnCode();
    if (!success) {
      state = "failed";
      doFailThing();
      return;
    }
    state = "store transaction record";
    storeTheThing();
    state = "complete";
  }
First, this code is a simple example, in reality, the code has bunch of branching and doesn't actually call out to functions to do things like "getCardDetails," so just replace that function in the example above with some parsing logic of a string to parse Track1 and Track2 data. The equivalent method we actually have in our code base to do that is ~1500 lines of code. But that string is doing nothing that a comment couldn't accomplish. Or more importantly, what the code could describe itself if it actually adhered to good design principles. Ideally, I would refactor this, but the owner of the company is adament on keeping this 1500 line abomination untouched.

For me, I often unused variables in production code are usually filling the void a comment or good design would have filled.

The good news is that although intuitively that feels horrible, mechanically those are string literals, so, that doesn't actually do very much, just re-assigning a pointer. Moreover, there's no way an optimising compiler can't see those assignments are futile and elide them, whereupon in release builds it might as well be a comment.

And yes, lots of people who don't like Zig's choice agree unused variables are bad and shouldn't survive into your release code. They just don't agree with Andrew that it's a fatal error and the program shouldn't build.

“Insane” really? How so? In what world do unused variables make sense?
I think forgetting to assign a variable to the return value and ignoring the return value are two different things.

Zig regards forgetting to assign a variable to a return value as a compile time error because there's an entire class of bugs that stem from it, e.g. resource leaks.

"that could one day replace C"

I don't understand why would anybody say that, it's seems such a simple concept to me that nothing ever will replace C. Think about just the Linux kernel, nothing else. Nobody will and can rewrite every part of it that a C compiler will not be needed.

The meaning is obviously to replace C in situations where C shines when writing new code. No one is talking about rewriting the Linux Kernel. Even the Zig project's explicit goal is to be seamlessly interoperable with C in order to build upon already written C code.
I doubt every single line of C will be removed considering a lot of it is drivers for hardware no one has anymore, but I’d say in 10 years the majority of actually used code in the Linux kernel will be Rust. The Asahi project has shown that Rust is the better choice for kernel code right now.
That will make backporting security fixes fun.

(I think there's less than a ten percent chance of this happening)

I'd say in 10 years the majority of new kernel modules will be written in Rust, but old code won't be rewritten just for the sake of rewriting in Rust (except maybe a select few where such a rewrite is identified as beneficial)
>> "It has very few keywords so it's a lot easier to learn than C++ or Rust.”

This is really the meat of it. They should be using a different language but struggle with two better choices.

Is zig used for Linux kernel at all?
I should hope not. Zig is not even 1.0 yet, using it on anything as relied upon as the Linux kernel would be a bad idea.

That said, I do all my projects in it these days if I can get away with it because I like it so much and it is reliable enough for me... well, except for the fact that the current master doesn't actually implement suspend/resume yet and I use that for coroutines.

It looks like Zig doesn't have a GC. Does it have any improvements over C in that area? Like Rust's lifetimes?
Short answer: not exactly, no.

Long answer: Zig does a lot of stuff to make the easy way of dealing with memory the correct way. The typical pattern is to use defer/errdefer statements to clean up on end of scope or in case of error. One of the included allocators checks for leaks and use after free, and it is trivial to change which allocator is used based on build mode. It is also very strict about pointers and has a whole lot of different kinds to represent different use cases and include appropriate safety checks (in safe and debug modes) when dealing with them.

Longer answer: I think it's inevitable that someone will write a static checker for lifetime analysis.
That’s pretty much impossible in the general case, thanks to Rice’s theorem. Rust can only do so because it limits the possible lifetime “shapes”.

Otherwise why don’t build it for C instead?

Rust doesn't do it either. It has 'unsafe'. So just as with rust, you would annonate certain things in zig as 'trusted', and anything else will be checked.

For example, for pointers, assume allocator interface create, alloc, destroy, and free functions work as advertised, and trust them. Anything that uses the pointers is checked. For file descriptors, assume open and close are trusted, everything in between is checked.

You can't really build it so easily for c because c language is not generally compiled to an IR, and the syntax is rather ill-defined. It's not context-free and it has lexical macros.

Anyways statically checked annotated c exists, SEL4 is a good example, but iirc it actually checks arm machine code, and yes it is memory safe C.

What do you think C compilers work on if not IR? Also, it not being context-free is not a problem after parsing, that’s a different part of the process.

This is about what we can claim about runtime properties. What exactly do you mean by a checked pointer? A pointer has temporal and spatial boundaries — will you add a huge amount of metadata to each pointer to runtime validate whether it’s correctly used? That’s pretty much what valgrind and sanitizers do, but that has quite an overhead. In general you can’t check it for any kind of pointer usage. Rust as I mentioned can get away with it by heavily restricting how pointers can be used. Unchecked is an escape hatch, you can’t just put the “checked” boundary at any place you wish, it has to be placed in a way that makes analysis of its assumed properties sound. In case of Rust this is true, but you would have to restrict Zig’s semantics to something like Rust’s to make your idea workable.

> In case of Rust this is true, but you would have to restrict Zig’s semantics to something like Rust’s to make your idea workable.

That's exactly the point. You would be restricting zig's semantics within code that you've fenced. You claim it's not possible. Trivially it should be possible, because this is simply the equivalent of abstracting rusts compiler logic (and in the worst case scenario even it's type system, via annotations) to a sidecar step that is run at a different phase of building your code. Or, more generically, formalizing logic that a code reviewer is doing in their head to understand data lifecycles. Really, to support your impossibility claim: It's on you to give an example of zig code that wouldn't be analyzable.

I think we misunderstand each other on what is “inside-outside”. Sure, you can have a tiny “safe” block in zig, but that won’t be able to use any outside pointer safely, and I don’t see much point in that — is that what you mean? Because unless you say that, then no.

Also surely you can write some zig to rust compiler (which mind you, will not compile most of your programs as the equivalent rust is not semantically correct), but there is not much point.

Re code:

    let ptr = allocate();
    if (undecideableProperty()) {
      destroy ptr;
    }
    print(*ptr);
Insert any number of undecidable property there, one elegant example would be the Goldbach conjecture.
If any branch could destroy the pointer, you wouldn't be able to use it later, the static analysis would stop the build. No need to evaulate the undecidable.
I tried using Zig for a little bit, and it just doesn't feel ergonomic to use _for me_. It's not 1.0 so I'm hoping that a lot of that will be ironed out, but some of it is baked into the language. Being forced to assign variables to _ if you aren't using them somewhere else in the program annoyed me a lot more than I expected. Especially in the learning phase when you are asking a lot of questions and have to write up toy examples of what you are trying to do when asking people for help.

I also ran into an issue where I wanted to initialize an array with 30k 0 values and I ended up having to write a loop to do it, and ran into issues with the loop and assigning values. It felt like if the language was going to be militant about certain things to prevent you from making mistakes, that it should just go the whole way. At least with Rust after fighting with the borrow checker I would know I had a safe program. With Zig I have to fight with the langauge, but then it's not even memory safe once I get everything working

   var arr: [30_000]u8 = undefined;
   for (&arr) |*x| x.* = 0;
That sounds like a loop.
you could initialize it to {0} ** 30000 instead

  var arr = std.mem.zeroes([30_000]u8);
P.S: Not a loop. The implementation of std.mem.zeroes for an non-sentineled array is:

  return [_]info.child{zeroes(info.child)} ** info.len;
Which in this case will be equivalent to:

  return [_]u8{0} ** 30_000;
Which uses the comptime-only `*` operator to expand the single-item u8 array with value 0 into an array of 30k 0s.
after seeing so many segfaults in the bun.sh project, I don't feel confident enough to use it.

Sure it allows you to move fast but at what cost

Are you still running into segfaults in recent versions of Bun? If so can you file an issue or provide some same code? We try to fix these quickly
genuine question: why might you pick Zig over Rust?
The borrow checker is a hard hill to climb for a lot of people, to the point where many devs get to that part of the tutorial and decide to go do something else with their lives.

  const std = @import("std");
  
  pub fn main() !void {
      const stdout = std.io.getStdOut().writer();
      try stdout.print("Hello, {s}!\n", .{"world"});
  }
To me, this looks worse than anything I have seen before.
Alternatively:

    const print = @import("std").debug.print;

    pub fn main() void {
        print("Hello {s}!", .{"World"});
    }
Or using the logging API:

    const info = @import("std").log.info;

    pub fn main() void {
        info("Hello {s}!", .{"World"});
    }
(I'm actually not sure why Zig doesn't have a simple way to print to stdout, but I guess there are 'reasons')
Zig is the ugliest of the Three Piglets (the other two being Rust and Swift).
And the Big Bad Go will come and blow down their houses.
you might not have a stdout, for example very low to the metal.
Tried Zig over a weekend, liked it, but not going to use it in real projects, yet.

Two concerns to remain for me:

    * version 1.0 release
    * bus factor, the major contributor seems still mainly a single person
I hope it can take off soon.