How we are going to sell Zig?

can you tell us a little bit more specifically why the zig version turned out so much faster than the c version? I’m curious what the control enabled you to do that is maybe not so ergonomic in rust or c (or say odin which i am learning right now too).

1 Like

100% agree with the sentiments of most here that the language sells itself. The great projects already built with zig (e.g., TigerBeetle, Ghostty) will help with that too, and I am sure new and future zig projects will continue inspiring people to pick the language up.

Personally I don’t see much appeal in making zig more memory safe than it already is. I am sure that there are other areas (outside of what I usually dabble in) where memory safety is is trickier to get right, but after the first few weeks of getting used to zig I have found I don’t have issues with this. Admittedly, I did struggle a bit in the early days - especially when it come to heap-allocated strings. I think C++'s std::string had engrained some bad habits, and learning zig exposed a genuine skill issue on my end. I think working through this helped me become a better programmer: I have a clearer picture of where the bytes live now. It also helps the language is very transparent with regards to this. Is memory safety a real challenge in larger-scale zig projects? It seems like there is some projection of this onto zig because it is seen as a modern C, and I wonder how much of that is really warranted.

4 Likes

Yeah no problem, first of all I want to preface that I have an equal amount of experience with both C and Zig, I’ve used both for about the same time, and about the same amount (although nowadays it’s more zig than C).

I also want to say that I’m the lead developer on the project, I’m alone working on it, and my boss only checks progress and formalize the specs what is the need. I also want to say that none of the things that I have implemented are necessarily optimal, neither the C or Zig implementation or perfect, they work good enough for our needs, but obviously it’s not universal

Furthermore I want to be clear none of the things that zig allowed me to do, are fundamentally impossible in other languages, you could technically do it, but practically speaking, it would either be very hard to get right consistently, very tedious to do, or very complex, and within the context of my team being, well just me “impossible”.

Next a lot of things are technically just syntactic sugar that makes it easier to get right, like i know pointer arithmetic and bit shifting, and i know a few intrinsic and can google to understand them, but fundamentally I’m human not a robot, and like everyone i suppose i have my fair share of daily off by one, wrong bit twiddling symbol or ops, didn’t or read the doc too fast to not mess up the intrinsic, forgot to check for null, or to be perfectly disciplined with the return code.

Finally, my hardware is a raspberry pi zero 2w running raspberry pi fork of linux, with an IMX477, the device has a 1400mamp/h battery and ideally should last 2h. The memory bus is 512mb of ddr2 and the processor is a cortex A53. Now in no particular order here is a dump of all the things that made the zig implementation faster and easier to get right and fast. (and the hardware limitation is that the CPU is fast, but the memory is garbage and very busy)

  1. this will come to nobody surprise but having slices backed into the type system makes it tremendously easier to do zero copy, and view of the data, it makes it much more ergonomic to reinterpret the bytes as something else, and because of the runtime bounds checking makes it evident that you messed something up.

eg

// FrameBuffer.Plane: DMA-BUF memory view
pub const Plane = struct {
    bytes: []u8 = &.{},    // fat pointer: ptr + len
    dma_fd: i32 = -1,
    offset: u32 = 0,
    stride: u32 = 0,
};

const luma = plane.bytes[0..plane.stride * height];
const chroma = plane.bytes[plane.stride * height ..];

and in C

struct lc_v4l2_buffer {
    void   *data;
    size_t  length;
};


void *luma = (uint8_t*)buf->data;
size_t luma_size = stride * height;
void *chroma = (uint8_t*)buf->data + luma_size;

// this is needed but I always seem to forget to add it because past me is stupid
assert((uint8_t*)chroma + chroma_size <= (uint8_t*)buf->data + buf->length);

in practice this means that the C version is harder to get right, and requires more discipline, and on top of that is easy to not get right.

  1. the next is obviously comptime, there are a ton of controls in v4l2 ioctl parameters, and in C you need to switch on everything to call the right parameter whereas in zig it’s just
pub fn fromAnon(values: anytype, out: *Values) error{UnknownField}!void {
    out.* = .{};
    inline for (std.meta.fields(@TypeOf(values))) |field| {
        if (!@hasField(Values, field.name)) return error.UnknownField;
        @field(out.*, field.name) = @field(values, field.name);
    }
}

which means i can just do something like cam.set(io, .{.exposure_time_us = 5000, .ae_enable = true}) and this just work and asign to the right value. whereas in C i have to do this.


typedef enum {
    CTRL_EXPOSURE_TIME_US,
    CTRL_ANALOGUE_GAIN,
    CTRL_AE_ENABLE,
    // .....
} control_id_t;

int camera_set_control(libcamera_t *cam, control_id_t id, void *value) {
    switch (id) {
    case CTRL_EXPOSURE_TIME_US:
        return v4l2_set_control(cam->fd, V4L2_CID_EXPOSURE, *(uint32_t*)value);
    case CTRL_ANALOGUE_GAIN:
        return v4l2_set_control(cam->fd, V4L2_CID_ANALOGUE_GAIN, *(float*)value);
    //.......

    }

}

and more inconvenient than the runtime cost, is because C doesn’t have exhaustive non overlapping enum, you can shoot yourself in the foot easily, and if you forget to add the branch somewhere you will be debugging your own stupidity for a very long time, until you realize that you just forgot something.

  1. the simd is so much more ergonomic and less tedious than the C intrinsic counterpart, and this means that I can easily compile my code on x86_64 without having to write an intrinsic or scalar implementation in C surrounded by ugly ifndef/define for somethign that I won’t use in prod anyway.
uint64_t compute_sum(const uint16_t *values, size_t n) {
    uint64_t sum = 0;
    for (size_t i = 0; i < n; i++)
        sum += values[i];
    return sum;
}

#include <arm_neon.h>
uint64_t compute_sum_neon(const uint16_t *values, size_t n) {
    uint64x2_t sum = vdupq_n_u64(0);
    for (size_t i = 0; i + 8 <= n; i += 8) {
        uint16x8_t v = vld1q_u16(values + i);
        sum = vpadalq_u16(sum, v);
    }
    return vgetq_lane_u64(sum, 0) + vgetq_lane_u64(sum, 1);
}

vs this there’s imo no debates.

const V = @Vector(16, u16);

fn computeChannelSum(values: *const [N]u16) u64 {
    var sum: u64 = 0;
    var i: usize = 0;
    while (i + 16 <= N) : (i += 16) {
        const chunk: V = values[i..][0..16].*;
        sum += @reduce(.Add, chunk);
    }
    while (i < N) : (i += 1) sum += values[i];
    return sum;
}
  1. thanks to the Io interface, my code trivially compiles on windows or linux x86_64 since my camera is an interface, I’ve implemented a camera implementation that is actually an ffmpeg loopback, which while doable in C would require compiling ffmpeg and cross-compiling it which is definitely something I don’t want to trouble myself, and even then I would still probably have to adapt the code for windows, which I also absolutely do not want to do. The io interface also allowed me to change my architecture, from an epoll base loop, with signals based on memfd, to a simple wait, which means that I went from 3 syscalls per frame to only 1. (although that one is a bit on me). But in general zig’std is super syscall efficient and that’s really important for the predictability of latency in my zig impl the p99/p95/p50 are all within 10% consistently while in my C code it’s probably double that.

  2. in the zig version I was able to also remove 500 if checks for null zero or what kind of error number a function returned because C doesn’t have error as value or tagged union.

  3. Thanks to the allocator interface, I’m also able to control precisely where the memory is, how it’s managed, aligned etc, which while feasible in C is tedious, error prone, and just plain annoying.

  4. I was able to get zero cost Tracy without having to make my code ugly with ifdef/ifnef

  5. In general the library has a better architecture, but it’s in great part enabled by the fact that Zig default path is very optimized and efficient and that optimizing further requires much less time and effort to both get fast and right, which isn’t the case in C it’s always hard to get things perfectly right. Also in C the true Zero copy path wasn’t really feasible, and I had one big memcpy that was absolutely painful and killed performance, because the memory bus on my target is garbage. (which killed the performance enough that I wasn’t able to reach a stable encoding frequence)

  6. forgot to mention but the std.Io.Reader/Writer are so clutch and so usefull to minimize double buffering and having a precise control of how and what to do with the data

  7. also forgot but inline in Zig ACTUALLY INLINE which when decompilling your code in C is sometimes infuriating that the compiler is like NOPE

Now like i said none of the “optimization” or things that i show here are impossible to do or replicate in C, but again getting them right, consistently, is hard, and it eats at the time that you can dedicate to performance optimization, my zig implementation is still far from perfect, but I’m extremely confident that it will get there.

Also to answer your questions I don’t know odin so i can’t speak for that, but I still have 17k loc of C dependency on a bluetooth library so i guess the lack of the native c compiler/cross compiler would probably be annoying but I just also don’t know the language enough to say.

As for rust, I also don’t know the language enough to say confidently that It couldn’t be done, but I’m doing a lot of very unholly 100% unsafe things, for example, I do my computer vision task directly in mmaped DMA which with zero copy because the memory bus simply can’t afford it, and i have exactly 16667us to do a mask + threshold + erode3x3/dilate3x3 + spaghetti8 ccl. this is the hot path and cannot use any synchronization primite outside of jesus benediction, this is where my C code wasn’t able to do that, and I had to resort to a memcpy but in zig i replaced the autogenerated decision tree that the spaghetti8 ccl needs (which is 3kloc btw) by a simpler labeled continue switch which now means that my upper bound under load is 12ms which is plenty. And in Rust this kind of lifetime wizardry would probably be hard to explain to the compiler

37 Likes

Wow, that response was much more detailed than I was expecting! Thanks for the insight. I definitely agree that just because something is possible to do in a language does not make it particularly enjoyable for the programmer, possible in a timely manner, cheap for the studio, or worth the engineering effort/resources.

To be honest though, I spent like 6 hours researching and writing a 2000 word response to your message where I went really in depth with the comparisons (particularly to Odin), but I feel that would be inappropriate for a forum/this specific thread. It’d make an interesting blog post, but I don’t have a blog! I wonder what I should do with it… Is anyone interested?

4 Likes

Well I wouldn’t say inappropriate as long as it’s a comparison with zig and within the topic of the conversation I don’t think it’s a problem

2 Likes

In practical terms, further improving C interop so that Zig can be used to replace C in more use cases. And improving the ergonomics of incrementally migrating from C to Zig

2 Likes

I haven’t tried Odin yet.

I would certainly be interested in your comparison from someone who also uses Zig.

1 Like

I would also be interested in reading your comparison… maybe you need a blog? Or you could throw it into a gist and link it from here, so those of us interested could click over without breaking everyone else’s flow, and without going to the trouble of setting up a whole website…

1 Like

Почему то мне кажется что стоит заняться Zig. А чутьё меня в таких вещах редко подводит. Я так много лет назад полюбил JS. И тут прям какая то любовь с первого взгляда.

3 Likes

I literally just joined this community and have taken the plunge in learning Zig. I think Zig sells itself and it cleverly infiltrated millions of code bases via it’s zig cc toolchain. I think a niche will carve itself out and there’s no need to sell it. However if it needs that number one eureka thing to stamp itself on the playing field like rust did with safety. I think it would be you’re safely in full control and the software people create with it will do the talking. I think once it hits 1.0 it will snow ball into popularity.

9 Likes

We need to up the language server game if we want to sell Zig beyond enthusiasts. Unless you believe in the bright future of agentic coding (which I don’t), the language server is still 90% the storefront of a language to the average dev. I hate to break it but a modern PL can only go as far as its tooling goes.

11 Likes

It kinda breaks my heart how true this is.

I think I’m immune because I’ve lived so long without things like ZLS, that the hassle of setting them up always seems to be worse than the benefit I’m missing. I’m happy just looking stuff up in documentation and running the compilation in the editor, jumping error message by error message.

3 Likes

Radical opinion … the LSP protocol that all these in editor language servers use is not perfect, especially with languages such as zig that offer comptime evaluation.

I think long term, Zig + the incremental compiler / linker could (should) just invent its own editor protocol that does the whole job properly, and break out of the limitations of LSP.

You could then bundle in - inline documentation Ala ‘zig std’, a repl, and a debugger. All served by ‘zig build’.

4 Likes

10 posts were split to a new topic: Desired Features of ZLS/Build Server Protocol

It’s only such a signal to the world because people in the Zig community decided to throw a hissy fit about it. Personally, I see the Bun.js situation as a success story. Thanks to Zig, they were able to get their project off the ground. They made a compelling product and was able to secure financing as a result. Now they’re graduating to a more corporate language. This sort of things happens all the time.

4 Likes

citation needed?

4 Likes

First time I’ve ever seen someone describe something as “more corporate” in a positive way. :grinning_face:


I think you have a point though. They seem to love making things corporate, and what’s more corporate than waterfall?

Before the rewrite, they were putting out releases roughly 2x per month, never more than 27 days apart. The last zig release was on May 13, and the ‘rewrite in rust with agents’ blogpost came on July 8.

This post was written on Aug 10 - 3 months with no new releases.

In the end, v1.4 dropped on Aug 20, 100 days after v1.3.14. So they stalled their development pipeline for at least 2 1/2 months. That’s really bad, but it gets worse:

Since v.1.4 there have been two more releases:

  • v1.4.1 on Sep 4
  • v1.4.2 on Sep 5

So that’s a gap of 15 days, 1 day, and 33 days (and counting).

That one day gap seems pretty suspicious, and indeed:

This release fixes two regressions impacting Elysia and AsyncLocalStorage, a hang in @discordjs/ws, CMYK JPEG decoding in Bun.Image, a rare JIT crash, and a GC crash on musl.

I was curious when the last time a 1-day release happened. The answer is ‘almost 2 years ago’: Dec 21, 2024 (v1.1.42).


Not shipping your product for 3 months and then putting out a borked release is not exactly a promising sign, and I am curious how long it will take for their development pipeline to recover - assuming it ever does.

10 Likes

Development of Bun probably slowed down because the original Zig programmers decided to cash out. That’s a success story in my book.

1 Like

The original guy is Jarred Sumner and he’s still there, but it could be true for other people that joined him. Still,

I can pretty easily imagine a successful port going through this stage, it’s not quite ‘evidence’ of a failed port … yet.

… but if I were a betting man (and I am) then I would wager that their troubles from this are not at an end yet.

3 Likes

Toxic comment

2 Likes