Zig cookbook: collection of simple Zig programs that demonstrate good practices(github.com)
github.com
Zig cookbook: collection of simple Zig programs that demonstrate good practices
https://github.com/zigcc/zig-cookbook
58 comments
Thanks for highlighting this. Internally we're mostly avoiding the parts of the std Reader interface that impose significant runtime costs if you use them, but it would be swell to upgrade the stdlib implementations to do something like golang interface upgrades for buffered readers.
The threads example (https://zigcc.github.io/zig-cookbook/07-01-spawn.html) doesn't run the threads in parallel. It spawns the first thread and waits for it to finish and then spawns the second thread and waits for that to finish.
The fix is to move t1.join() to after the second thread is created.
The fix is to move t1.join() to after the second thread is created.
I just started C and Zig at the same time in October. While Zig was not easy to learn right away, in my opinion, it has become a joy to work with as I've become more familiar with it. This isn't to say learning C is a useless exercise in futility, but I think people not understanding why Zig is cool might need to remember they may be more experienced and more accustomed to things about C that are hard to get used to as a beginner.
honestly I still don't get why I need zig, modern c seems just fine for daily coding, what are the must-see selling points that can attract c programmers to switch?
Is someone telling you you need zig?
- No preprocessor nonsense
- typesafe metaprogramming (usually no void* type erasure)
- sane legibility of complicated types
- (for beginners) not having to learn/deal with make
- corrects the long long mistake (yes, I get that everyone uses inttypes.h now)
- alignment aware pointer types
- null aware pointer types
- null terminated string aware pointer types
- slices (fat pointers?) default
- sane cross-compilation
- errors returned as values, no errno weirdness
- no pass-by-value/pass-by-reference ambiguity (const correctness by default)
At least four of these are footguns my highly experienced coworkers would have avoided this last week of work alone.
It's possible that as a longtime c developer, you have internalized the biggest problems with c, especially the ones that trip up beginners. Maybe zig is not for you. If you start using zig, you just may start wanting to throw your desk out the window every time you have to go back to c, which is not really worth the effort/employmemt sanity risk.
- typesafe metaprogramming (usually no void* type erasure)
- sane legibility of complicated types
- (for beginners) not having to learn/deal with make
- corrects the long long mistake (yes, I get that everyone uses inttypes.h now)
- alignment aware pointer types
- null aware pointer types
- null terminated string aware pointer types
- slices (fat pointers?) default
- sane cross-compilation
- errors returned as values, no errno weirdness
- no pass-by-value/pass-by-reference ambiguity (const correctness by default)
At least four of these are footguns my highly experienced coworkers would have avoided this last week of work alone.
It's possible that as a longtime c developer, you have internalized the biggest problems with c, especially the ones that trip up beginners. Maybe zig is not for you. If you start using zig, you just may start wanting to throw your desk out the window every time you have to go back to c, which is not really worth the effort/employmemt sanity risk.
Is make considered bad or something? I use make and I mostly program in Go. It seems to make my life pretty simple.
What is an alternative?
What is an alternative?
> Is make considered bad or something?
Depends what you're using it for. Generated from CMake, Makefiles are OK I guess, although Ninja is strictly better these days.
Manually written, I would say make is bad.
1. It doesn't promote portability: since each target calls shell commands, it is very easy to accidentally create a non portable Makefile (something alleviated when using CMake that handles the portability before emitting the Makefile)
2. It is imperative and stateful in subtle ways, which is the wrong level of abstraction for a build system. Systems that are declarative and describe the desired state of the build are much easier to use and reliable
3. It is bare bone as far as build system functionality goes: it doesn't provide many of the useful things I expect from a modern build system: the concepts of library dependencies, build profiles, compiler flags, test, documentation or even project in general... are all orthogonal to make
4. It is ad-hoc: as a result of the previous points, the expected functionality of a build system is often re-created in the makefile, but in a non fully standard way (conventions exist, but must be manually enforced and are limited). This results in brittle and difficult to use build systems.
> What is an alternative
Language-specific package managers, notably Cargo.
If using rust, there's also a pattern called xtask[1] that's aiming at providing a standard interface in the form of a binary (since it is rust, it is portable, but most of the other points unfortunately apply).
Otherwise, CMake, meson, nix, bazel, buck2
[1]: https://github.com/matklad/cargo-xtask
Depends what you're using it for. Generated from CMake, Makefiles are OK I guess, although Ninja is strictly better these days.
Manually written, I would say make is bad.
1. It doesn't promote portability: since each target calls shell commands, it is very easy to accidentally create a non portable Makefile (something alleviated when using CMake that handles the portability before emitting the Makefile)
2. It is imperative and stateful in subtle ways, which is the wrong level of abstraction for a build system. Systems that are declarative and describe the desired state of the build are much easier to use and reliable
3. It is bare bone as far as build system functionality goes: it doesn't provide many of the useful things I expect from a modern build system: the concepts of library dependencies, build profiles, compiler flags, test, documentation or even project in general... are all orthogonal to make
4. It is ad-hoc: as a result of the previous points, the expected functionality of a build system is often re-created in the makefile, but in a non fully standard way (conventions exist, but must be manually enforced and are limited). This results in brittle and difficult to use build systems.
> What is an alternative
Language-specific package managers, notably Cargo.
If using rust, there's also a pattern called xtask[1] that's aiming at providing a standard interface in the form of a binary (since it is rust, it is portable, but most of the other points unfortunately apply).
Otherwise, CMake, meson, nix, bazel, buck2
[1]: https://github.com/matklad/cargo-xtask
Cool! What would you suggest for go?
Fwiw I develop on a Ubuntu workstation. I build and deploy to mostly Ubuntu or bare bones Linux.
Fwiw I develop on a Ubuntu workstation. I build and deploy to mostly Ubuntu or bare bones Linux.
> It seems to make my life pretty simple.
Presuming you're doing anything more than the bog-simple "check age of file, maybe compile, maybe link":
How does it make the life of someone who has to read or debug your code?
Presuming you're doing anything more than the bog-simple "check age of file, maybe compile, maybe link":
How does it make the life of someone who has to read or debug your code?
[deleted]
Many of these are improvements on C++, too. Zig is pretty compelling in the space.
C++ has all of these warts too as it's roughly speaking a superset of c. Yes I'm aware it's not an exact superset.
I think you can get tripped up by all these things I have listed in c++.
I think you can get tripped up by all these things I have listed in c++.
Man the more I think about it there are even more:
typedef NAME struct {};
vs
struct {} NAME;
(Did I even get that right?)
- Namespacing in c is a nightmare, and c++ mangling is nuts.
- sane way of importing code, no header/code bifurcation
typedef NAME struct {};
vs
struct {} NAME;
(Did I even get that right?)
- Namespacing in c is a nightmare, and c++ mangling is nuts.
- sane way of importing code, no header/code bifurcation
those are good points, thanks!
hope it will eventually include a http server and ssl into std for 1.0 release since many applications are now networked together.
I will wait for 1.0 to try it again.
The team is deliberately pushing back 1.0, they really want 1.0 to be as best possible a finalized version of the language they are happy with. You may be waiting for a long time.
You should also know that the way you're engaging seems very concern-troll-ey.
Why do I say that?
> hope it will eventually include a http server and ssl into std for 1.0
Would you consider holding C to that standard?
Like zig already has way more stuff in its std than c, including json support and crypto primitives.
You should also know that the way you're engaging seems very concern-troll-ey.
Why do I say that?
> hope it will eventually include a http server and ssl into std for 1.0
Would you consider holding C to that standard?
Like zig already has way more stuff in its std than c, including json support and crypto primitives.
daily job is all c and c++ and Zig is something I need find time to get good at weekends and nothing critical for immediate needs, but yes I'm interested.
Why shouldn't the unexperienced systems programmers go "all in" by using Rust, then? The first priority for them should be memory safety. Not to mention that Rust has more ergonomic features and stronger ecosystems.
Why should the first priority be memory safety? If the first priority is always memory safety, then rust should not have `unsafe` keyword. Existence of `unsafe` implies that, even rust, something is more important than safety
And, maybe rust is a poor choice because the language encourages layering hidden complexity and architecture astronauting.
And, maybe rust is a poor choice because the language encourages layering hidden complexity and architecture astronauting.
> If the first priority is always memory safety, then rust should not have `unsafe` keyword.
You can't have a system programming language without some unsafe layer because eventually you will have to interface with either the OS or a library written in another language and that's unsafe. Even languages like Java and C# have unsafe escape hatches for situations like these.
You can't have a system programming language without some unsafe layer because eventually you will have to interface with either the OS or a library written in another language and that's unsafe. Even languages like Java and C# have unsafe escape hatches for situations like these.
It seems then rust's first priority is "being a systems programming language", with memory safety being at best second.
Okay yes, the ability to print to stdout, an ultimately unsafe operation, is more important than upholding Rust's ideal of an entirely safe programming language.
Because gov agencies say so?
Also, memory safety vulnerabilities is the largest class of vulnerability by count today by a large margin (70% of the total) and cost a lot of money to bigger companies.
As changing language stacks also cost a lot of money, there needs to be a strong incentive to do so. Getting memory safety without compromising applicability to the lowest levels of the stack is one such incentive.
Rust also doesn't have a lot of drawbacks, from my experience (professional use for years, coming from C++) and from recent reports from Google[1] (in particular, the learning curve steepness[2] and productivity penalty[3] are overstated).
[1]: https://opensource.googleblog.com/2023/06/rust-fact-vs-ficti...
[2]: Anecdotally, these ramp-up numbers are in line with the time we’ve seen for developers to adopt other languages, both inside and outside of Google. (Same report)
[3]: Overall, we’ve seen no data to indicate that there is any productivity penalty for Rust relative to any other language these developers previously used at Google. (Same report)
Also, memory safety vulnerabilities is the largest class of vulnerability by count today by a large margin (70% of the total) and cost a lot of money to bigger companies.
As changing language stacks also cost a lot of money, there needs to be a strong incentive to do so. Getting memory safety without compromising applicability to the lowest levels of the stack is one such incentive.
Rust also doesn't have a lot of drawbacks, from my experience (professional use for years, coming from C++) and from recent reports from Google[1] (in particular, the learning curve steepness[2] and productivity penalty[3] are overstated).
[1]: https://opensource.googleblog.com/2023/06/rust-fact-vs-ficti...
[2]: Anecdotally, these ramp-up numbers are in line with the time we’ve seen for developers to adopt other languages, both inside and outside of Google. (Same report)
[3]: Overall, we’ve seen no data to indicate that there is any productivity penalty for Rust relative to any other language these developers previously used at Google. (Same report)
> Why should the first priority be memory safety?
Because it's easy for beginners (and even experts) to create a memory safety bug, which is more critical than most bugs.
> If the first priority is always memory safety, then rust should not have `unsafe` keyword.
Err.. I suppose you don't fully understand how memory safety in Rust can be achieved. Every safe abstraction (function, trait, etc.) depends on unsafe code, either directly or transitively.
What's the difference between a safe function and an unsafe one, then? A safe function won't violate memory safety, no matter what combination of arguments passed to it.
Because it's easy for beginners (and even experts) to create a memory safety bug, which is more critical than most bugs.
> If the first priority is always memory safety, then rust should not have `unsafe` keyword.
Err.. I suppose you don't fully understand how memory safety in Rust can be achieved. Every safe abstraction (function, trait, etc.) depends on unsafe code, either directly or transitively.
What's the difference between a safe function and an unsafe one, then? A safe function won't violate memory safety, no matter what combination of arguments passed to it.
Whether first priority is memory safety depends on a great many things about the software being created.
Mainly because GC is easier on inexperienced (and, really, all) programmers than linear types.
[citation needed].
Here's a reference to the contrary[1]:
> Overall, we’ve seen no data to indicate that there is any productivity penalty for Rust relative to any other language these developers previously used at Google.
[1]: https://opensource.googleblog.com/2023/06/rust-fact-vs-ficti...
Here's a reference to the contrary[1]:
> Overall, we’ve seen no data to indicate that there is any productivity penalty for Rust relative to any other language these developers previously used at Google.
[1]: https://opensource.googleblog.com/2023/06/rust-fact-vs-ficti...
> [citation needed].
GC is undeniably easier to work with, at least at first, and the comment I was responding to was about "inexperienced programmers". No bleeping citation needed because it's my opinion see. I'm not saying that GC is better than linear types (it's not), but apparently the very thought that GC might be easier on "inexperienced programmers" is enough to send HN commenters into a tizzy!
GC is undeniably easier to work with, at least at first, and the comment I was responding to was about "inexperienced programmers". No bleeping citation needed because it's my opinion see. I'm not saying that GC is better than linear types (it's not), but apparently the very thought that GC might be easier on "inexperienced programmers" is enough to send HN commenters into a tizzy!
That link is about learning rust.
It’s quite obviously true, that you have to actively think/maintain how long each instance lives, which is simply done by the runtime in your place in case of a managed language.
In my experience, this is not a big effort at writing new code, but at refactoring/maintenance - as lifetimes are parts of the public APIs even.
It’s quite obviously true, that you have to actively think/maintain how long each instance lives, which is simply done by the runtime in your place in case of a managed language.
In my experience, this is not a big effort at writing new code, but at refactoring/maintenance - as lifetimes are parts of the public APIs even.
> That link is about learning rust.
While this is true for most of the article, I think the quoted sentence is about more general Rust use.
> In my experience, this is not a big effort at writing new code, but at refactoring/maintenance
In my experience working on the Meilisearch codebase, Rust is the easiest language to refactor, because it catches so many errors at compile time:
1. Most languages lack the `mut` binding modifier that allows to warn when the binding does not need to be mutable (catching errors when part of the code hasn't been updated)
2. Most languages lack exhaustive initialization, destructuring and matching, that catch pretty much all the situations where a new field has been added to a struct.
3. Let's not speak about languages with null, or without static typing
Having more type information (including lifetimes) makes refactorings easier, not harder. I concede it causes a bit more boilerplate but that's not the bottleneck when writing code.
While this is true for most of the article, I think the quoted sentence is about more general Rust use.
> In my experience, this is not a big effort at writing new code, but at refactoring/maintenance
In my experience working on the Meilisearch codebase, Rust is the easiest language to refactor, because it catches so many errors at compile time:
1. Most languages lack the `mut` binding modifier that allows to warn when the binding does not need to be mutable (catching errors when part of the code hasn't been updated)
2. Most languages lack exhaustive initialization, destructuring and matching, that catch pretty much all the situations where a new field has been added to a struct.
3. Let's not speak about languages with null, or without static typing
Having more type information (including lifetimes) makes refactorings easier, not harder. I concede it causes a bit more boilerplate but that's not the bottleneck when writing code.
Not really, because it covers only memory while linear types cover all types of resources with a limited lifetime. GC won’t help you with „this operation is valid only within that context”, e.g. you can write to a file only as long as it is open.
I’m struggling with your assertion that Zig’s meta-programming is ‘typesafe’. I am very aware of its benefits compared to C’s ‘preprocessor nonsense’ (i.e. textual replacement ‘macros’), but I don’t think the normal definition of type safety covers how Zig’s meta-programming functions (I am assuming you’re talking about comptime and using Type arguments for things like generics). I am certainly able to understand how comptime expressions look well typed and safe compared to C, but, AFAIK, Zig’s comptime expressions are on par with C++ templates in Type Theory terms.
Big caveat here being, I don’t see anything wrong with the implementations of this sort of meta-programming in Zig. It is in some ways analogous to the difference between static and dynamic typing, there is a PCWalton tweet explaining his view on this somewhere [1], particular regarding generics across languages. And while I take a different stance on the issue, I do think the tweet is a decent example. Zig’s meta-programming works well and errors will be caught during compilation at some point, I just don’t think it meets the technical (probably academic-centric) definition of type safety. I think it would take a comparison to something like Template Haskell to actually find type safe macros (MetaML came say earlier, but is not very well known).
— I’m certainly open to being wrong about this from a technical standpoint, so feel free to disagree and maybe direct me to better/clearer documentation.
1 — https://twitter.com/pcwalton/status/1369101878642941953 :: There is a follow up tweet using more academic terms in a Type Theoretic setting, but I couldn’t find it quickly on mobile
Big caveat here being, I don’t see anything wrong with the implementations of this sort of meta-programming in Zig. It is in some ways analogous to the difference between static and dynamic typing, there is a PCWalton tweet explaining his view on this somewhere [1], particular regarding generics across languages. And while I take a different stance on the issue, I do think the tweet is a decent example. Zig’s meta-programming works well and errors will be caught during compilation at some point, I just don’t think it meets the technical (probably academic-centric) definition of type safety. I think it would take a comparison to something like Template Haskell to actually find type safe macros (MetaML came say earlier, but is not very well known).
— I’m certainly open to being wrong about this from a technical standpoint, so feel free to disagree and maybe direct me to better/clearer documentation.
1 — https://twitter.com/pcwalton/status/1369101878642941953 :: There is a follow up tweet using more academic terms in a Type Theoretic setting, but I couldn’t find it quickly on mobile
Yes for typesafe generics I mean
"equivalent to template <T> in c++"
For sure there are type-uhdsaff generics in zig. Allocator is probably the biggest example.
"equivalent to template <T> in c++"
For sure there are type-uhdsaff generics in zig. Allocator is probably the biggest example.
> no pass-by-value/pass-by-reference ambiguity
There's a known design issue [1] with one feature in zig that can cause issues with value/pointer passing and can cause aliasing errors. The goal for the language is to resolve the problem before 1.0, even though the fix will likely cause code to feel a bit closer to C. There's a talk [2] that goes into details on why this is an issue.
Regardless, I still love zig.
[1] https://github.com/ziglang/zig/issues/12064
[2] https://youtu.be/dEIsJPpCZYg
There's a known design issue [1] with one feature in zig that can cause issues with value/pointer passing and can cause aliasing errors. The goal for the language is to resolve the problem before 1.0, even though the fix will likely cause code to feel a bit closer to C. There's a talk [2] that goes into details on why this is an issue.
Regardless, I still love zig.
[1] https://github.com/ziglang/zig/issues/12064
[2] https://youtu.be/dEIsJPpCZYg
Hmm, there's a distinct change of opinion by andrewrk from the linked issue (2022) to now
https://github.com/ziglang/zig/issues/12064 (July 2022):
> Hope you don't mind I'm going to reopen this issue because it concisely demonstrates a design flaw with result location semantics and I'm not ready to accept status quo as permanent due to aliasing issues like this.
Compare to a very closely related issue regarding destructing, which exhibits the same problem
https://github.com/ziglang/zig/issues/17283 September 2023):
> This is working as designed. Everybody was aware this behavior would be a consequence of destructuring and there is no plan to change it.
https://github.com/ziglang/zig/issues/12064 (July 2022):
> Hope you don't mind I'm going to reopen this issue because it concisely demonstrates a design flaw with result location semantics and I'm not ready to accept status quo as permanent due to aliasing issues like this.
Compare to a very closely related issue regarding destructing, which exhibits the same problem
https://github.com/ziglang/zig/issues/17283 September 2023):
> This is working as designed. Everybody was aware this behavior would be a consequence of destructuring and there is no plan to change it.
Genuine question: what do you mean by ‘daily coding’?
I certainly find C acceptable, if not compelling, for doing widely portable, memory and speed sensitive software engineering, but that is not what I would typically associate with ‘daily coding’ tasks. I would normally use that term to signal thing like data, task, and OS automations (i.e. scripting, command line convenience apps, text/binary file compression/search/reorganization).
Not knocking your answer, just trying to get your intended meaning to evaluate whether I agree with the particular use case as a fit for a language as minimal and manual as C.
I certainly find C acceptable, if not compelling, for doing widely portable, memory and speed sensitive software engineering, but that is not what I would typically associate with ‘daily coding’ tasks. I would normally use that term to signal thing like data, task, and OS automations (i.e. scripting, command line convenience apps, text/binary file compression/search/reorganization).
Not knocking your answer, just trying to get your intended meaning to evaluate whether I agree with the particular use case as a fit for a language as minimal and manual as C.
I do embedded stuff so C is the primary language, sometimes C++ as well.
That’s cool, for you C is in fact a daily coding language and I’m probably a little jealous that you get to do low level, performance critical work regularly.
In short: zig offers a massive productivity boost, while delivering less buggy and more efficient software.
The long history of C has demonstrated pretty clearly that it's not possible for humans to write C (at scale) that is free of vulnerabilities.
I could list features, but as an otherwise happy C programmer I can confidently state that Zig makes it substantially easier to write fast, correct programs, be sure they're correct, and maintain those two properties as I modify those programs.
A smattering of points to look at include comptime (replacing the preprocessor [0], scripted lookup tables, generics, ...), non-null pointers, the community ethos of a principle of least power (e.g., any part of the stdlib you'd expect could be used on an embedded device can in fact be used there), exhaustive switching, cross-compilation, and errdefer.
[0] Not being textual is the source of comptime's power, but you'll never do something like write Zig code that looks like Fortran. Comptime adds some capabilities and takes away others. The tradeoff is pretty positive IMO.
A smattering of points to look at include comptime (replacing the preprocessor [0], scripted lookup tables, generics, ...), non-null pointers, the community ethos of a principle of least power (e.g., any part of the stdlib you'd expect could be used on an embedded device can in fact be used there), exhaustive switching, cross-compilation, and errdefer.
[0] Not being textual is the source of comptime's power, but you'll never do something like write Zig code that looks like Fortran. Comptime adds some capabilities and takes away others. The tradeoff is pretty positive IMO.
Zig might not be the answer, but so isn't "modern" C, if security is something one cares about.
If it is only about being a more portable Assembly like language, provided the CPU is a PDP-11 descendent without any additional compute units like SIMD or GPU, then yeah, C might be good enough.
If it is only about being a more portable Assembly like language, provided the CPU is a PDP-11 descendent without any additional compute units like SIMD or GPU, then yeah, C might be good enough.
C has a better SIMD story than Rust or Zig currently.
Not really, some C compilers have language specific extensions that exposed SIMD, C as defined by ISO and POSIX, does not.
Rust has them as part of the language, as does .NET, D, as will C++26 get, eventually Java as well,...
Rust has them as part of the language, as does .NET, D, as will C++26 get, eventually Java as well,...
Rust hides operations like register-register vpshufb behind unsafe despite this instruction having no implications for memory safety and crashing on CPUs that don't support it (just like auto-vectorized "safe" code built for a different set of extensions than the CPU it's running on).
Zig formerly exposed a subset of SIMD operations via LLVM intrinsics, but this was a bug and was fixed, so we now must use inline assembly to recreate immintrin.h and arm_neon.h.
Zig formerly exposed a subset of SIMD operations via LLVM intrinsics, but this was a bug and was fixed, so we now must use inline assembly to recreate immintrin.h and arm_neon.h.
Doesn't change the fact that neither immintrin.h or arm_neon.h. are listed on ISO C, or POSIX standards.
Does this matter? Rust and Zig have no standard. Each has fewer compilers than C has compilers that ship these headers.
In short: Zig can (and does) fix C warts from the ground up because it doesn't need to be backward compatible with C, while still getting the essence of what makes C useful (many other new languages don't get this important part). The biggest advantage over C for day-to-day coding is probably the much more complete and useful stdlib. You also don't need to "switch", mixed C/Zig projects are well supported by the Zig language and build system. Having said that, I still write most new code in C (C99 designated init is still superior to what Zig has to offer for struct initialization), but at least there's now a worthwhile alternative on the horizon to switch to in the future.
> C99 designated init is still superior to what Zig has to offer for struct initialization
Can you explain this for the ignorant?
Can you explain this for the ignorant?
When initializing a struct, Zig currently doesn't allow chained designators and also doesn't allow to partially initialize arrays and have the rest of the array filled up with default values (the first one isn't all that important, but the second is).
E.g. the closest Zig equivalent to this C99 code:
https://github.com/floooh/sokol-samples/blob/b3bc55c4411fa03...
...is this:
https://github.com/floooh/sokol-zig/blob/a4b3c287fadd153a504...
...note how part of the initialization had to be moved out into separate assignments, which means the struct can't be created as const.
There's a ticket about this here, but it's currently not high-priority:
https://github.com/ziglang/zig/issues/6068
Zig also isn't alone in this, I haven't encountered any other programming language yet which has such a flexible struct initialization syntax as C99.
Odin is probably the closest I've seen yet.
https://github.com/floooh/sokol-odin/blob/084c2429bd52b8d9dc...
E.g. the closest Zig equivalent to this C99 code:
https://github.com/floooh/sokol-samples/blob/b3bc55c4411fa03...
...is this:
https://github.com/floooh/sokol-zig/blob/a4b3c287fadd153a504...
...note how part of the initialization had to be moved out into separate assignments, which means the struct can't be created as const.
There's a ticket about this here, but it's currently not high-priority:
https://github.com/ziglang/zig/issues/6068
Zig also isn't alone in this, I haven't encountered any other programming language yet which has such a flexible struct initialization syntax as C99.
Odin is probably the closest I've seen yet.
https://github.com/floooh/sokol-odin/blob/084c2429bd52b8d9dc...
For that specific case, it seems like the array should be a packed struct (whatever needs it to be an array can receive an array via @bitcast). These features of C designated initialization seem great though.
There's also another solution to use slices instead of 'embedded arrays', but with slices you suddenly need to think about ownership (e.g. what happens with the referenced slices when the top-level struct is copied).
I agree that it's worth avoiding slice members for this reason if the slices are the only thing that would stop memcpy'd copies of the struct from "just working."
In Zig, while this example does use a BufferedReader, the `streamUntilDelimiter` is implemented on the more generic `std.io.Reader` and thus cannot take full advantage of the buffering. It checks 1 byte at a time, and cannot leverage the SIMD-optimized `std.mem.indexOfScalar`.
Of course, you're free to create your own `readLines` that works against a BufferedReader. I do hope we'll see a whole version dedicated to review/polishing of the standard library. And maybe more work on composition, abstraction and interfaces.
The issue is described here: https://github.com/ziglang/zig/issues/17985