Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Zig cc: A drop-in replacement for GCC/Clang (2020) (andrewkelley.me)
301 points by AlexeyBrin on July 18, 2021 | hide | past | favorite | 121 comments


D is taking a different approach. D is getting a builtin C compiler (called ImportC) that is a from scratch C compiler, now in prototype form.

https://dlang.org/spec/importc.html

It works by having an extra module called cparse.d

https://github.com/dlang/dmd/blob/master/src/dmd/cparse.d

which parses Standard C. The lexer and semantic routines of the D compiler are then tweaked to support C semantics. The same optimizer and code generator is used.

As a side effect of using the D compiler's innards, ImportC can do things like handle forward references, and even execute C code at compile time!


I miss playing with D. It looks like you've done a lot of neat stuff recently, but I just don't have a project that I can use it for. I remember being thoroughly impressed with its metaprogramming capabilities in college when I was working on dproto.


This is getting super-weird, love it!

One assumes it is because C is so hard to rid ourselves of because POSIX (etc) that you're even entertaining this idea?

I mean, "it's fun" could also work as motivation, I guess.

I'm just curious!


I admit it's been fun! My old C compiler (Datalight C) works well, but its coding practices are severely outdated. I wanted to see how it would look using modern practices.

P.S. I wouldn't be in the business if it wasn't fun.


A decent subset of D programmers have used C and have been majorly bitten by it before, so it's definitely not a marriage of love put it that way. ImportC is, in my view, about optics and a little convenience - using C with D is already trivial if you understand how C works, ImportC just makes it offensively easy to do.

There are some less theoretical things too, i.e. no more keeping kernel headers up to date (apart from ones that use inline ASM that we have to be careful with).

With ImportC, D will be one of the better and non-hackier ways to interface C with other languages - i.e. introspection and metaprogramming but in a real language rather than some brain-dead DSL


The reasons for ImportC are:

1. It's no longer necessary to translate .h files to D to interface with C. While doing the translations isn't hard, it is tedious, and if there are a lot of .h files it becomes a barrier.

2. .h files get updated over time, and correspondingly updating the D files is error prone. It's much more effective to just compile the new .h files.

3. It is not necessary to even create a .h file - just compile the .c file with ImportC and D will access it just like any other module. D will even inline the C functions in it.

4. Use it as a standalone, small and fast, C compiler.


I'm glad D is finally delivering on its promise. I remember looking at it years ago disappointed that "you can use C from D!" meant writing and maintaining wrapper libraries for every damn thing.


I’ve been learning Zig for a few months now and what I’m in love with is the ability to just use Zig to interact with whatever C libraries I want, but doing so with the conveniences that Zig offers.

I want to use SDL2 and cairo and pango? No problem! GTK4, easy peasy! There’s a C lib that comes as a single .c file that makes it easier to interact with SDL? Drop it on the project folder and compile it with Zig along all the other .zig files!

As somebody that has been avoiding C as much as I can, I’m loving this approach since it has open the doors to the world of C for me without having to learn autotools, make, valgrind, et al


Omg, skip the rest but valgrind is amazing and requires almost zero learning to use. Build your code with debug symbols, find inputs that trigger a segfault, then run it under valgrind. It then reports out of bounds memory access, with a full stacktrace. It's got a billion more uses, but at the most basic, that one use-case will keep you from wasting time futzing with a debugger or print-debugging 99% of segfaults and memory leaks.


Have you tried ASAN? If you like valgrind, you will be in love with ASAN, it's really surpassed it at this point IMHO.


I have tried ASAN, and I'm fully prepared to love it, but last time I tried to use it with Cython, it was pretty fidgety to configure my build system to get it running -- unlike valgrind, you need to understand how programs are compiled and linked. If you want ASAN to see into the Python that's calling/consuming your Cython, you need to build a custom Python. It's certainly a sharper tool, but valgrind has the learning curve of a hammer.


Ugh yeah, it just doesn't work well with Python. A friend of mine went down the same rabbit hole trying to debug a C extensions... it's a shame.


Yeah, I'll be honest... I know how to build Python and all that, but I stopped dead once I realized that it was a requirement. Much faster to just valgrind/[print-]debug an issue than set up all that tooling. Making all that happen is on a distant back-burner, as it will take about 10+ uses to pay off and segfaults are typically rare and shallow for me.


Be careful with ASAN on cryptographic code, though. It can introduce side channels.


ASAN is not meant to be used in production though. Or I missed something in your comment.


I've seen it advised to periodically release to production with ASAN/UBSAN/TSAN enabled, with the intention that it's only 1/nth of a deployment, and even then, only deployed for enough time to collect instrumentation. I don't have a citation to share, so take with as many grains of salt as you feel appropriate.

If nothing else, I appreciate the advisement that it can have security implications.


> with the intention that it's only 1/nth of a deployment, and even then, only deployed for enough time to collect instrumentation

You can't unleak secrets.


I agree with you, but I don't understand intent of your comment.


If you leak all the secrets 1% of the time, that's not leaking 1% of the secrets. All of the secrets have been leaked, albeit over a short stretch of time.


That's definitely a good point, and worth thinking about. But most people don't go to the trouble to rebuild dependencies like openssl. Unless you're actually implementing crypto, it seems unlikely you'd expose anything, although you do have to consider the possibility that a crypto library could call an instrumented function which could leak state.


ok, I'm down!

Using zig I finally decided to try GDB and it is amazing!


This is one of the things I really liked about working with Swift. You just got 50 years of C libraries for free, with high level features on top of it, which means there is a lot you can do.


Note that none of autotools, make, and valgrind are required for C. You could be building with meson and ninja.

Valgrind is also more useful than just for C, although admittedly it's most useful for binaries that have parts written in unchecked languages like C.


Right. Even in an entirely safe language you can have leaks, and valgrind is an effective way to find those leaks if you can afford the virtualisation overhead.

If you can't afford the virtualisation overhead, and you need to find leaks you should try what Raymond Chen suggests in "The poor man's way of identifying memory leaks" (not bothering to link since Microsoft will only move it anyway, they have several times since I read it). If you are too lazy to do it by hand, or find the technique works but wish it less manual, this is what Leakdice does:

https://github.com/tialaramex/leakdice OR https://github.com/tialaramex/leakdice-rust


http://bytepointer.com/resources/old_new_thing/20050815_224_...

(a site that tries to keep a proper backup of the ever moving writings of Chen)


As much as I dislike C from safety point or view, all of that is also easy peasy with C.


I really like what they've done for cross-compilation. Automatically selecting appropriate libc/crt implementation is such a relief.

Compared to Zig, cross-compilation in C/C++/Rust seems downright user-hostile: you have to obtain the correct cross-sdk, but it won't tell you which one, or how to get it (some combos don't even exist), or even where to put it. If you don't guess everything correctly, it will fail with inscrutable errors, or worse, only fail at runtime on the target machine.


What this kind of stories always miss is that the host platform needs all system libraries for the target platform for anything beyond the basic examples using the standard library.

E.g. try to cross compile Metal shading example for iOS from Linux with zig, or do a Win2D UWP rendering application from Linux with zig.


The Metal shader compiler is an unfortunate special case, because the bytecode and the Metal compiler tools are proprietary and locked up by Apple (there are precompiled binaries of the Metal compiler for Windows, but not Linux). The ball is definitely in Apple's court here, there's nothing any language project can do, except illegally reverse engineering Metal bytecode, which obviously isn't a good idea.

The system API problem can be solved by shipping automatically generated API bindings (similar to what Microsoft does with the Rust bindings to the Windows APIs, a similar project for Zig is underway). Once those bindings exist, no Windows SDK is needed anymore and cross-compiling should "just work".

PS: Zig actually ships with the mingw headers in the default installation (e.g. windows.h, d3d11.h, dxgi.h etc are all there), I haven't actually tried to cross-compile a Windows executable using the Win32 APIs though, but it looks like it should work already.


> illegally reverse engineering Metal bytecode

Reverse engineering is legal in the US (unless bound by contract).


The metal compiler probably could run with darling


So does every new release of any Windows component require a new version of Zig? That doesn't seem sustainable.


I don't actually know what Windows components mingw supports in detail and how often mingw is updated because of that, but I guess that the important Win32 APIs are stable. I suppose that the new APIs which are highly in flux could be supported by separate packages, like this: https://github.com/marlersoft/zigwin32)


I'm not sure what's really "missed", if you call a library other than the stdlib of course you're going to need to provide the appropriate headers/libraries for that as that's got nothing to do with a C compiler.


In general, it isn't so clear when these kinds of examples are posted.

And to go back to my examples, headers and libraries alone won't make it, without the remaining toolchain to create the appropriate package.


Well for your particular examples UWP is C++/C#/a few others but it has no direct c binding and iOS APIs are all defined for Objective-C and Swift so sure, you're not going to get far with a C compiler before you even start thinking about a c cross compiler.

If you want to cross compile C stuff e.g. a Win32 GUI app with Direct X or get OpenGL bindings for SDL though those all work just fine if you provide the SDK files same as you would compiling natively. The advantage being now you can do all builds from a single build instance just by feeding it the libraries and a target platform.


Right. Are you going to ship all Windows headers and import libraries to everyone, including DirectX, etc.? Are you going to keep them up to date with every new version of a Windows component? Are you going to ship parts of the macOS, or iOS SDKs?

I don't see shipping all targets all the time to be a particularly scalable solution in general. When you start including all libraries necessary to develop actual apps, the download size will eventually get so big that people will complain. Just make it easy for developers to get the targets they need, but don't ship everything by default.


I'd argue it's less about what the compiler does by default, but rather what it (and the community) enable.

For Mach (the Zig game engine I'm working on), I have a Zig build script which fetches the required parts of the macOS and iOS SDKs from GitHub to enable cross compilation from Windows/Linux -> macOS. Zig's linker, zld, enables that to work easily.[0]

For SDL.zig, they have also achieved cross compilation for most targets out of the box[1].

Contrast this to say, Go or Rust, where most GLFW and SDL bindings require mucking around with the system dependencies. It doesn't just work out of the box, I think that's really unfortunate and hope other languages work more towards painless installation of C-binding dependencies.

[0] https://twitter.com/slimsag/status/1413770578494775298

[1] https://github.com/MasterQ32/SDL.zig#support-matrix


> Just make it easy for developers to get the targets they need, but don't ship everything by default.

It seems to be what Flutter is doing, and it must be a ton of work.


I'd guess once Zig gets its package manager many target-specific packages can move out of the default installation.


Absolutely, which is exactly what I'm arguing for. :)


I've had to compile only a few times C/++ programs, and the experience can't be really compared to Rust (probably, to any other language :)).

Lately I've cross-compiled a few times Linux/Windows programs, and the only thing I had to do (assuming that the underlying crates are compatible, and that the development libraries are in place, which is required for any language) was:

  rustup target add x86_64-pc-windows-gnu
  rustup toolchain install stable-x86_64-pc-windows-gnu
That's all. Then one can compile with:

  cargo build --target x86_64-pc-windows-gnu
I've also compiled for RISC-V, and it was the same procedure.

There are surely issues with individual crates, and possibly with lower-tier targets, but to characterize this procedure as "user-hostile" doesn't make sense.


That's true only for static libraries and pure-Rust projects. For executables and dynamic libraries Rust relies on system-provided linker (except nightly that can enable lld) and target libc already present on the OS (the nice part zig has, but Rust doesn't).

Rust totally craps out if you try to make a binary or dylib and you don't already have cross-compilation toolchain on your OS. It's doable from a Linux distro that has cross packages and a GNU target. It's a PITA for anything else, including anything from Windows and macOS hosts.

`zig cc` is actually useful even for cross-compiling Rust projects, because some Rust crates depend on C libraries.


> Compared to Zig, cross-compilation in C/C++/Rust seems downright user-hostile:

This has been the status quo for so very long. Usually these projects are maintained by different teams. IMO musl is more accessible than glibc and that's part of what makes it easier to distribute. Arm and x86 coexisting for as long as they have is a factor, and so is Kelley's ambition to make this use case well-supported. nostdlib is also well-supported by Zig btw.


It's always a bit harsh comparing a 36 year old language with more-or-less full backwards compatibility, with a new language designed with the benefit of hindsight.

That's not to say that it's impossible to do something like that in a C++ compiler but it's going to be more work than there is benefit. Tangentially related: https://twitter.com/__phantomderp/status/1416108650809241601

C++ also tries to bring in modern programming research when it makes sense, but that also explains why it's like an octopus with extra legs nailed on.


The octopus analogy seems especially appropriate. The thing about the legs on an actual octopus is that there's a bunch of local compute substrate in them, if you were to nail extra legs onto the octopus those are rubbish compared to its real legs which can think.

For example, move semantics are a peg leg for the octopus. Should an octopus have a peg leg? I'm not a biologist of any sort, but my guess is "No". I can see why C++ wanted to add move (without this performance in some scenarios is just awful) and yet since C++ didn't start out having move semantics and can't give up backward compatibility it has to shoehorn them in with this awkward "hollowed out object" idea. The result is performance still isn't where it could be and the feature is unergonomic.


I would not include Rust in that list, in my experience rustup and cargo are miles ahead of C/C++ when cross compiling.


Personally, I see it in three tiers: Rust improved upon those experiences, but I still wish someone would implement what Zig has in rustc.


Yup, fully agreed Zig stands above others here. Cross compiling C/C++ is just a special kind of hell. Each target always has some sort of bespoke toolchain and build environment constraint that add so much unneeded complexity.


I don't. We could approximate it by shipping all targets by default, but few really want that and the few who do can just install those targets with rustup.


Sure, like many things, I bet you could pick three or four and get 90% of the default coverage. That still helps 90% of the users.

I also think you're really missing out on the "compile C code easily too" thing. That isn't very pleasant at all in Rust today.

(Finally, one reason it’s so big is that we ship binaries. This ships source. std-aware cargo could really help here.)


> I also think you're really missing out on the "compile C code easily too" thing. That isn't very pleasant at all in Rust today.

So much this. Cross compiling pure Rust code is really easy today but as soon as you have a C dependency in your dep tree, you're gonna have a bad time.


Is it really few who want that? Anybody developing server software on windows or mac and deploying on linux could use this. Anybody developing cross-platform client software could use it.

I'd guess that a lot of Rust developers currently do development on their target platform, but that's because cross compiling is such a pain. If it were as easy as zig or go then things might well be differen.


My x86_64 Rust libraries directory is 86MB. `rustup target list | wc -l` spits out 84 for me. The math doesn't add up.


Is rust cross-compilation really is straightforward as just installing the target triple and then it "just works" (including linking and C FFI compilation)?

My understanding was that most people were using "cross" docker images for "cross-compilation", because setting up native actual cross-compilation was quite involved.


No, it isn't that easy. pcwalton is misrepresenting things here IMO.

Yes, people, including myself, use 'cross' precisely because it isn't as easy as "just use rustup to install the desired target." 'cross' has a bunch of Docker images that set up the cross compilation toolchains, and those images represent a fair amount of domain knowledge that I don't have to tediously acquire to use it.


I don't know much about cross compilation, but in terms of the Rust case, could part of it be the relative complexity of the language compared to Zig?


`zig cc` cross-compiles C, not Zig.


No but I mean why the Zig cross compilation story is better than Rust according to this thread


Nothing about 'zig cc' is related to language semantics, it is entirely a toolchain feature. Language semantics do have some bearing on cross compiler difficulty, but IMHO it's not a major factor, especially within the sorts of languages that Rust and Zig are. The largest factor I can think of (compile-time code execution) exists in both Zig and Rust. It's possible I'm forgetting something, though.

It doesn't exist in Rust because nobody has proposed it or implemented it, simple as that.


I think Zig does cross compilation (ignoring for the moment, projects with C dependencies) better because it comes out-of-the-box with the libraries and headers needed to target a lot of systems. In Rust, you typically get additional toolchains via rustup, whereas Zig works on all of the targets with a single install.


Absolutely, hence a toolchain thing.


Are you sure that we should be shipping all targets by default into one monster download? This doesn't seem particularly scalable.


The whole toolchain for Zig, which appears to include complete cross compilation, is less than 40MB compressed[0]. I would hardly call that a "monster download".

I think developers can spare that much space. Besides, storage space is scaling much faster than the number of available targets, so I don't see how this could be "not particularly scalable" unless you either have a weird definition of "target" or predict there's going to be a sudden explosion in the number of targets.

Go also makes cross compilation to any supported target a breeze in the default toolchain download, and it is an incredible convenience for the developer.

[0]: https://ziglang.org/download/


The Xcode download, which is the only officially-supported way to develop native apps on macOS and iOS, is 9.8 GB and gets larger with every update. The Windows 10 SDK is 4 GB and likewise is only getting larger. Sure, you can develop some apps with only a subset of the SDK. But, as a language project, do you really want to be in the business of optimizing and subsetting every single OS SDK?

I think a lot of the issue here comes down to targeting server workloads and command-line tools vs. apps using more of the OS libraries. In practice, Go focuses on the former kinds of apps, so shipping all targets is a more viable approach for them. But when you start getting into apps that want to use a larger swath of the platform facilities, then the size of the platform SDKs starts to become an issue.


No one (that I've seen) is suggesting to include those SDKs, which you probably don't even have the rights to redistribute anyways.

I provided both Zig and Go as examples of what people are looking for, and their downloads are neither obscenely large nor do they include those massive SDKs.

So yes, you have a weird definition of "target" that no one else here is using, and it is disingenuous for the conversation. rustup can already add additional targets after installation, are you suggesting this downloads 10GB SDKs? Definitely not, and that sounds irrelevant. (Even once you do this, the toolchain experience is not as good as what Zig or Go offer.)

Compare what Zig and Go do to what Rust does, not some strawman argument that would require users to download 10GB of SDK with the Rust toolchain. Alternatively, find me someone who is saying they think these multi-gigabyte SDKs should be included, because I don't see that anywhere in this conversation.

People here are complimenting what Zig currently does, which takes less than 40MB compressed. Zig can cross compile to Windows just fine. There are limitations to everything, but what Zig and Go offer is strictly better than what Rust offers in terms of the out-of-box cross compilation experience, and it isn't unduly burdensome on the developers like your proposed 10GB download.


My nightly-x86_64-unknown-linux-gnu lib directory is 148MB. This is because it includes several libraries in both rlib and so format (static vs. dynamic linking), asan/lsan/msan/tsan variants, and so forth. You could easily imagine that ballooning to 1GB if we shipped all tier 1 targets. This is what I mean by shipping all targets not being scalable: it might have seemed so in the early days when the libraries were smaller and the targets fewer, but not now.


Yes, the Zig (and Go) developers have clearly put a lot of effort into ensuring the size of their toolchain remains reasonable. I fully believe the Rust developers could achieve similar results as well, if they really wanted.

Until then, the lack of a great cross compilation experience out of the box is just a limitation of the Rust toolchain. It's an acceptable limitation in many situations, but I don't buy your repeated arguments on this HN discussion that it is unclear that this is desirable to fix. Clearly many people in this discussion alone disagree with your position, and my own anecdotal discussions with other developers in real life aligns with this discussion. YMMV, obviously.


> Yes, the Zig (and Go) developers have clearly put a lot of effort into ensuring the size of their toolchain remains reasonable. I fully believe the Rust developers could achieve similar results as well, if they really wanted.

I strongly disagree. These are different languages. Rust leans heavily on generics and monomorphization. It implements most language operations, like ptr::offset, in the language itself, increasing the size of library metadata. It supports tools like the sanitizers. It has a rich serializable MIR format so that generics can be embedded at a higher level than just machine code and a lower level than source.

It is absolutely not true that nobody "really wants" smaller binaries. Rust already did a fair bit of experimentation with running crate metadata through gzip, etc. years ago (turns out there are some thorny tradeoffs around compilation time vs. on-disk storage when you go that route). I can't speak to Zig, knowing less about it, but with Go there were conscious language design decisions that favor binary size over runtime performance (e.g. hash table lookups all going through a single function instead of being specialized). This is fine! But it's contrary to the idea that Rust could achieve smaller binary sizes if we "really" wanted to.

> Until then, the lack of a great cross compilation experience out of the box is just a limitation of the Rust toolchain.

Rust has a great cross-compilation experience. It's as simple as:

    $ rustup target add arm-linux-androideabi
    $ cargo build --target=arm-linux-androideabi
That's it. One more command than Zig or Go, to install the toolchain you need, and then you're off to the races.


The difference is really not just one command. I suggest you go out and actually try Zig cross compilation or at least read more about it. Andrew Kelley has written a lot about it. The work impressed me so much that it was one of the things that provoked me to become a financial supporter of Zig. (Even though I haven't written a line of Zig yet.)

Cross compilation in Rust is better than C or C++. But it's still a big pain.

I really think you are underestimating what the Zig folks are doing. Please investigate more deeply.

In particular: "Zig does not ship with any pre-compiled libraries; instead it ships with source code, and builds what it needs on-the-fly."


Do you think that Rust would be a better language if everything had to be compiled from source?


Obviously I don't. And that's totally consistent with learning from Zig about how to improve the cross compilation story for Rust. (Trivially: compile some things from source, but not other things.)

I'm honestly pretty disappointed at your participation in this thread. It seems like you're going out of your way to assume the worst possible interpretation of what folks are saying.


The Ultimate++ environment for C++ does the same thing with C++, and it seems to work just fine even for them.


If you use the build-std nightly feature you can compile the entire standard library, even for a custom JSON target file, and it goes pretty quick. With a bit of attention that feature would be just fine at filling in the gaps in rustup’s prebuilt targets.

But Zig’s approach isn’t actually about making everyone compile more things from scratch. It doesn’t compile libc from scratch any time you select a new target. Reading the article you can see the extreme lengths it goes to to avoid this.

The main thing that’s missing from Rust is target-specific libc.so/etc, and an appropriate linker for every target. If you try cross compiling anything, you will soon run into this problem; your system will not have the correct target libc or an appropriate linker. You can generally only change one part of the triple before the experience starts falling apart. I can cross compile for iOS/tvOS/etc on my Mac; that’s about it.

Zig manages to solve this for all its targets by pre-processing simplifications of the various libcs, which are then bundled into the Zig binary (as 3 very small files). It generates a useless but linkable .so file from the preprocessed files for any target on the fly. It gets you the correct libc headers. Then it uses LLD instead of making you hunt around on Ubuntu forum posts finding and downloading the correct GNU linker for your specific architecture. So you don’t link to a real libc, but you don’t have to compile one either. (Until you want to execute a binary with QEMU, but usually non-simulated target machines have a libc.so already.)

(Aside: Golang solves this by not depending on libc at all, by reimplementing most of it including the syscalls in Go, and I believe by using its own multi-target linker. Many pros and many cons, but an approach Google is happy to sponsor.)

If you’re saying this approach would be infeasible for Rust std/core, then yes obviously. It’s statically linked, you can’t get away with a fake .so file.

But that was never really the issue. Rustc can build std’s rlib files for any target effortlessly. And yet cross compiling is still a pretty poor experience. The state of the art for Rust developers at the moment is rust-embedded/cross, which solves the same libc+linker problem that Zig does, naively, by literally using Docker and per-target Ubuntu images to download prebuilt GCC and libc packages from Apt. Basically we can do way better than that, because Zig showed us how.

See this Dockerfile for what I mean: https://github.com/rust-embedded/cross/blob/master/docker/Do...


> That's it. One more command than Zig or Go, to install the toolchain you need, and then you're off to the races.

Except it's not the same. Zig also cross-compiles C code beautifully, and lots of Rust code depends on C libraries. That "one additional command" does not solve cross compilation to an equivalent degree as Zig.

Go code tends to avoid C because of the heavy penalty that CGo imposes, so the cross compiliation experience is usually good there, but for different reasons than Zig.


100% sure? No. Mind you, I'm on MSVC, where I already need an extra ~1gb download to get the system linker.

We could still do a few different things to make this story better for users, even if it's not literally identical.


I see. I'll let the real experts add some insight but I don't think language complexity has much to do with it, considering Rust compiles to an IR first anyway. And I would say Rust has a good cross-compilation story as it is. In 2016 (!) I managed to get Rust to compile my SDL2 game to a mipsel device that used a custom GCC toolchain (http://www.gcw-zero.com/develop)


> cross-compilation in C/C++/Rust seems downright user-hostile: you have to obtain the correct cross-sdk, but it won't tell you which one, or how to get it (some combos don't even exist), or even where to put it.

Since advent of multiarch, crosscompilation does not seem harder than regular compilation. Just install package with gcc for target arch and required libraries for target arch (using the same way as gcc and libraries for host arch).


Another thing that's really cool, here: they've published the language (and toolchain) to PyPi. So, with a simple

    pip install ziglang
You can do

    python3 -m ziglang cc my_c_prog.c -o my_c_prog
    ./my_c_prog
A great way to do compilation on a system without needing to get a C toolchain installed, if you already have Python available


I literally started learning Zig today after watching a talk on youtube by Andrew Kelly. The drop-in C compiler replacement lured me.

But what surprised me immediately and the thing I'm absolutely loving is the error handling (https://twitter.com/mgill25/status/1416720958988210177) and the comptime features.

I've done Rust and while I love what it's doing, getting into low-level C-world without the overhead of a language like Rust feels liberating.


The thing I dislike about Zig error handling is that there's no way to associate informational context with an error. Bubbling the errors up, you'll eventually print out a string to the console that says "error: AccessDenied". The sysadmin/user running your software will be left baffled. Access denied... to what?!

Zig is 90% of the way there with error handling, but it is extremely obvious to me that you need to be able to have error context carried with the error... it is apparently not obvious to the core Zig team[0], and this is a major impediment to my interest.

Sure, if you're compiling to a microcontroller, maybe you're willing to sacrifice usability to avoid error values ever allocating... but most developers can spare an allocation for errors, especially given that error handling is generally not the hot path.

[0]: https://github.com/ziglang/zig/issues/2647 (among other issues)


It's obvious that error context is vitally needed. What's not obvious is how to accomplish this in the language without compromising other things such as simplicity. Pick any solution, and a new set of problems rear their ugly heads. That's not to say it can't be solved. The proposal is open; neither rejected nor accepted.

For especially difficult language design decisions, I like to work on other language changes that I am more confident about as a way to anchor the design, and then revisit the difficult questions, hopefully armed with more guidance from the rest of the language design being in place.


Totally agree; it's almost perfect, but this is defnly needed to get to full perfection.

While you can argue (as people do on that issue) that there are workarounds to carry error payload, the truth is if you don't make it ergonomic, many libraries will not do it, and that will filter down into software using those libraries, thus hurting the final resulting software.

I'm hopeful this feature will eventually get added.

(See also https://news.ycombinator.com/item?id=25798578)


> it is apparently not obvious to the core Zig team

I think it really is not obvious once you start thinking about all the implications. Right now the recommended way of reporting extra information alongside errors is by using a "diagnostics" struct.

You can see an extremely effective example of that in zig-clap, for example.

https://github.com/Hejsil/zig-clap

Also this pattern was also mentioned in the issue you mentioned: https://github.com/ziglang/zig/issues/2647#issuecomment-5898...

The advantage of this system is that the caller can decide whether error diagnostics are desirable or not. If they're not, then zig-clap doesn't waste cycles nor memory keeping track of the context in which an error happened. It's the best of both worlds in my opinion.


To add one more comment... you should study the distribution of votes in that thread. Take a poll of the developers using Zig. Anywhere you look, I'm sure you will see that most people want error handling to include context by default.

84 upvotes for the initial issue is substantial in a community the size of Zig.


> I think it really is not obvious once you start thinking about all the implications.

No... it really is obvious. I've written quite a bit of C, C++, and Rust over the years, in addition to all sorts of garbage collected languages. I'm not writing this as an off-the-cuff thought. I've even written a similar comment before on HN about Zig's error handling. I've thought about this for a long time. It is just as obvious now as it was on day one.

The fact that the Zig team keeps resisting this is just mind blowing to me. "Penny wise, pound foolish" is my opinion. The actual runtime cost here would be so entirely negligible, but its absence is a major limitation.

> The advantage of this system is that the caller can decide whether error diagnostics are desirable or not. If they're not, then zig-clap doesn't waste cycles nor memory keeping track of the context in which an error happened. It's the best of both worlds in my opinion.

Instead, the developer has to waste mental cycles and memory keeping track of the context on every single function call. Strongly disagree on it being the best of both worlds.

For starters, every library has to decide whether they will support it or not. If the language doesn't make this a built-in part of the core error handling, then spoiler: they won't. They'll throw away all useful error context, leaving no option for the application developer to add it back. Once the context is gone, it's gone for good. You can't know what a library did to cause an AccessDenied error... you can't reach into the library and pull that context out of thin air. If every library is implementing this diagnostics pattern, then that's an equally strong argument for it to be supported at the language level. Either context is being irrevocably lost or developers are wasting time writing convoluted code. Either way, this is not the optimal solution.

I have experience writing code in a lot of languages... error context is extremely important, and the "waste" of CPU cycles and memory is extremely minimal for the benefit gained.

As I have hinted before, a compile-time switch to throw this information away could be a valid solution for the extremely-difficult-to-imagine situations where great error handling is unacceptably costly on performance... then it would be like Zig's "async" support, where the library developer doesn't have to color their functions based on how the end user will use it. They should return all the error information all the time, and the compiler decides whether to keep the context or throw it away and provide only the error "name". Yes, this would bring its own challenges, but seriously, this option to discard the context is completely unnecessary outside of like 8-bit microcontrollers, and probably isn't worth implementing. Just provide context all the time. Please... it's such a small ask.

Looking at the links you've provided, which you have also provided in past discussions here, that pattern is extremely painful. I cannot overstate how much worse that usability is compared to the regular error handling Zig provides, and so obviously no one will ever use it. It serves as a pattern for the most desperate situations, and as a way to handwave the lack of language-level support for error context, in my opinion.


I'm still not convinced, but at the same time it's not up to me to decide. If you haven't done so already, I recommend you make your case in the relevant issue.


`zig cc` is also amazing to compile C and C++ code to WebAssembly. For standalone WebAssembly or WASI.

No need to install yet another toolchain, this is just another target Zig can compile to, out of the box.


Actually you can do the same with clang.

Emscripten just installs a bunch of stuff to make it easier to use existing libraries.

If you just want to use standard library, no need for extra stuff.

https://surma.dev/things/c-to-webassembly/


Only for freestanding webassembly. In order to target WASI, you have to install the builtins (https://github.com/jedisct1/libclang_rt.builtins-wasm32.a), as well as the WASI libc, and fiddle with `—-sysroot`.

Zig can target both without having to install anything else.


Assuming that you even need them.

Zig packages them, naturally you don't need to install them a second time.


From Release Note 0.8 Roadmap [1],

>The major theme of the 0.9.0 release cycle will be stabilizing the language, creating a first draft of the language specification, and self-hosting the compiler.

So I am guessing we could see 1.0 release within 12 months time? Or will there be 0.10 or 0.11 after that?

I am just eager to hear people's comments and opinions for using it in production within their context and edge cases.

And as a sort of side note, I quite like the momentum and the way Andrew has been handling Zig's PR. I just hope and pray HN keep that tradition, dont over hype it.

[1] https://ziglang.org/download/0.8.0/release-notes.html#Roadma...


> So I am guessing we could see 1.0 release within 12 months time? Or will there be 0.10 or 0.11 after that?

1.0 is going to take a lot longer than that.


I guess at least 2 major things after 0.9 would be package manager, library standardization and cleanup which would take many releases and perhaps need to be done before 1.0


What's usually not mentioned is that zig cc is also an ObjC/ObjC++ compiler, which is important on macOS to talk to system framework APIs. Zig 0.8.0 still has some warts on macOS (for instance one needs to tell Zig to use the Apple system linker), but those will be fixed in 0.9.0 (AFAIK it already works in the main branch).


> Take a moment to appreciate what just happened here - I downloaded a Windows build of Zig, ran it in Wine, using it to cross compile for Linux, and then ran the binary natively. Computers are fun!



interesting paragraph on why the builds are so small.

> You can download a 45 MiB tarball, unpack it, and you're done. You can even have multiple versions at the same time, no problem......Compare this to downloading Clang, which has 380 MiB Linux-distribution-specific tarballs. Zig's Linux tarballs are fully statically linked, and therefore work correctly on all Linux distributions. The size difference here comes because the Clang tarball ships with more utilities than a C compiler, as well as pre-compiled static libraries for both LLVM and Clang. Zig does not ship with any pre-compiled libraries; instead it ships with source code, and builds what it needs on-the-fly.


Zig looks really compelling. Anyone use it in production?


This person has been for many months now: https://www.youtube.com/watch?v=124wdTckHNY

We hope to be using TigerBeetle which is written in zig in production in 2022: https://github.com/coilhq/tigerbeetle


I looked at Zig a little while ago. What I really liked was some of the clever features of the language and thr C interop. What made me not pursue it for now is that I mostly use C for embedded development and Zig doesn’t have meaningful support for the ESP8266 platform. Also, not fully memory safe which obviously not the end of the world for me but if I’m going to switch away from C then I kind of want that as a thing.


`zcc` would be even better.


Having "cc" as a sub-command with a space has caused some issues with Go (cgo), make, autotools, etc [0].

The latest proposal from andrewrk is a new zig subcommand which would automatically generate wrappers ("zig-cc", "zig-c++", etc) for for the target, then set them in the current env ("CC=zig-cc", "CXX=zig-c++") [1].

[0]: https://github.com/ziglang/zig/issues/8716

[1]: https://github.com/ziglang/zig/issues/8973


  $ alias zcc='zig cc'
Only works interactively, though.


  shopt -s expand_aliases
Put that at the top of your bash scripts and aliases will work there too.


Nice!! I've been programming shells for ~30 years and I learn this only today :-(


I don't understand the difference. Is "zig cc" more smart to find included files than the other compilers?


zig distributes critical stuff like C library headers and libs. So it's not smarter, just more convenient. Built in support for several OS/architecture combos.


In addition, zig cc uses a remarkable compile+cache mechanism, which makes it possible to distribute a single build that can be used to cross-compile anything. It will cross-compile the necessary files as you need them, transparently - without downloading a new toolchain for each target, and quickly.

zig cc is really, really cool, and it's incredibly generous that it provides zig's benefits to users of C and C++ as well.


When you put it that way, this is an unfair comparison and the downsides are more obvious.

With GCC and Clang, I only pay for cross compilation if I need it, so for a fair comparison, the cross-compiler libraries should be installed for GCC and Clang. Also with GCC and Clang, I don't have to worry about a third party (the Zig team) incorporating bug and security fixes from upstream. And with GCC I get support for more platforms.

If I have chosen to use cross-compilation, building for alternative architectures is just as easy as for Zig.


Although as mentioned in the article, this won't work on systems like *BSD, Solaris or macOS.


One tiny correction that we do support seamless C/C++ cross-compilation to macOS now with the advent of our own in-house linker `zig ld`. :-)


The article goes into great detail of the differences.


Can it be used for c++ code? Also we build LLVM tip of tree from scratch. Is it easy to get the version of zig cc built against the newer version or does one have to wait for the project authors to update to the latest (I’m assuming my it’s a non-trivial amount of work?)


> Can it be used for c++ code?

Yup! https://github.com/ziglang/zig/commit/db17c0d88c44183b306099...

> Also we build LLVM tip of tree from scratch...

I would have also thought it would be non-trivial (Julia uses LLVM and has some non-backwards compatible patches) but this paragraph intrigued me:

>> At the time of this writing, LLVM 10 was just released two hours ago. It will take days or weeks for it to become available in various system package managers. But you can already download a master branch build of Zig and play with the new features of Clang/LLVM 10. For example, improved RISC-V support!

I found this for LLVM 12 (released April 14) https://github.com/ziglang/zig/commit/5a3ea9beced660c9cc463b... https://github.com/ziglang/zig/commit/e248de93a07fd6a7f48581... https://github.com/ziglang/zig/commit/6a7f21d1b8ca5c082c804e... https://github.com/ziglang/zig/commit/1438c324a460789f1004a3...

Andrew seem to be following along their release pretty closely.

Disclaimer: I don't know squat what I'm talking about


It does look impressive! Does anyone know if Zig exposes some APIs to extract/manipulate/analyze the AST of the C program? It might be a nice alternative to LLVM, which I find a little overwhelming.


zig cc uses LLVM under the hood.


right now - my understanding is once the self-hosted compiler is done, LLVM will be optional.


I think LLVM will still be required for release builds.


I believe the plan is to make LLVM optional for compiling Zig code, but features that work with C code will still require Clang to be compiled in.

Who knows, though. Someone might end up implementing a C compiler on top of the self-hosted Zig compiler's backend!


From what I understand, LLVM will still be used for release builds, while the self-holsted compiler will be used for quick feedback.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: