Juicy main is awesome :)

(…we need a ‘Praise’ category heh)

I finally got around updating my emulator toy project to all the latest Zig goodies, and holy shit does the juicy-main idea work well (mainly the pre-constructed allocator and IO system). I think this is exactly the right general direction, provide convenient sensible defaults but allow to override/ignore them.

Porting the file-IO code was also much less painful than expected thanks to the std.Io.Dir and std.Io.File namespaces (but I only use Dir.cwd(), Dir.readFileAlloc(), Dir.createFile() and File.writeStreaming() - it’s great that those basic file operations have ‘convenience wrappers’.

That is all :slight_smile: Oh, project is here: GitHub - floooh/chipz: 8-bit emulator experiments in Zig

(I haven’t tested on x86 for a while - be aware that there is a known issue with debug-mode performance and the new x86 backend: a massively big switch statement in the CPU emulator is compiled to a linear search which tanks performance).

29 Likes

Thanks for sharing, love learning from code in the wild that’s using new api’s.

I’ll use the opportunity to praise the big fat Io vtbl design, which makes writing helpful debugging/timing Io wrappers real easy, such as an API-level strace-ish decorator:

[170µs] dirOpenFile(handle:3, sub_path: "in.dat", mode: read_only)
[269µs] sleep(duration: 500000000ns)
[503708µs] fileReadPositional(handle:3, buffers: 1, offset: 0)
[503872µs] fileReadPositional(handle:3, buffers: 1, offset: 1)
[503908µs] fileClose(handle:3)

I think we’ll see a lot of powerful tooling thanks to this design.

6 Likes

Sadly didn’t have much time recently to play around with juicy main.

However i would like to praise the new HTTP/S Client and Server in std.
I know its a bit old news now, but the 0.15 Reader/Writer transformed that API from being almost unusable to really pleasant to work with.

Really excited to test it with the new IO once i have more time.

2 Likes

Huh, I’m not up to speed with the latest developments but for a personal project I recently made a richmain module to abstract away the same stuff… :sweat_smile:

1 Like

OMG these screenshots :slight_smile: What a cool project.
It reminds me of my Lemmix (DOS lemmings clone) which does it kind of the other way around: clone the lemmings mechanics and graphics for a windows program. On our monitors of today 1 dos pixel is 12 pixels now.

1 Like

I was initially very skeptical about Juicy main, however I have found myself warming up to the idea over time. That said, I’m still waffling on its granularity.

Like, it could work like this:

main.zig

pub const init_options: std.process.InitOptions = .{
  .args = true,
  .gpa = true,
  .arena = false,
  // etc.
};

pub fn main(init: std.process.Init) void {
  // init has args and gpa, but arena is void.
}

std/process.zig

pub const InitOptions = struct {
  args: bool = false,
  gpa: bool = false,
  arena: bool = false,
  // etc.
};

pub const Init  = struct {
  const init_options = @import("root").init_options;

  args: if (init_options.args) std.process.Args else void,
  arena: if (init_options.arena) std.mem.Allocator else void,
  gpa: if (init_options.gpa) std.mem.Allocator else void,
}

With a setup like this, the options provided could be things other than true or false in the event additional configuration is desired. Such as hinting to io where it should target on the balance between resource usage and speed. A parsed_args field on InitOptions could be a ?type, removing the need for start looking to see if main has a second argument for that purpose and allowing that to sit inside the main init structure.

The advantage of the above is that it would make it significantly more likely to be configurable to target the actual ideal. As it stands, I find it likely that as a project progresses it will want to move stuff out of juicy main and into the actual main, and there is little granularity for doing that.

The disadvantage of the above is that it’s more complicated than the current implementation, and might still not actually solve the problem of proper granularity. Eg, if you have an argument that might affect parameters when setting up your io instance. Which leads me back to my original suggestion of having it be a template.

So, all in all, I do feel that Juicy main’s design space is a slippery slope towards more complexity. Where I’ve warmed to it is that it’s a reasonable way for most projects to start, which is pragmatic. There are a bunch of things about Zig where being pragmatic wins over purity; They tend to get attention from people outside the core team (eg, files only being structs), but they do make the language productive to use by keeping simplicity and ignoring cases which have reasonable alternative solutions.

tl;dr: I’m about to init a new project, and I’m going to use Juicy Main, even if I don’t think it’s perfect, which is pretty awesome.

3 Likes

lol, didn’t even know juicy main was a thing :slight_smile:

I kind of fall into a similar pattern anyway - main() just declares a whole pile of plumbing stuff, puts it into a struct of type App, then calls app.run()

that’s pretty much how most larger Go apps work in the wild. I see juicy main takes that to the next level, good stuff.

Also - re the comment about stdlib http, yeah that’s getting real nice now. I have accidentally created a whole framework around stdlib for my web backends, and it really didn’t take much code at all.

I’m actually enjoying the breaking changes in 0.16 now (I update my compiler once a week max), and most of time the breaking changes fall into the “well thank goodness they changed that, the new api lets me remove a pile of mess in my code”

keep em coming !

1 Like

Some more granular config would be nice, but only to a certain point, after that it’d just be easier and more readable to just do the boilerplate in a non-juicy main.

I think a slightly nicer config api would be similar to DebugAllocator

fn main(init: std.process.Init(.{ .arena = false })) void {}
//minimal
fn main(init: std.process.Init(.minimal)) void {}

This way the config is better associated with main, even if you make an alias you can just goto-definition, you can’t do that with the init_options decl approach.

4 Likes

i think sticking to the current lack of granularity that you observe is probably going to be helpful to avoiding config complexity. once a project matures enough to dislike juicy main, it should be a piece of cake to juice your own main.

5 Likes

I think there’s also a large chunk of tools which will never need to change the defaults. For instance when I’m whipping up a small UNIX-style cmdline tool for processing some files I want convenience above all else. The most likely thing that would need custom handling is arg parsing though (e.g. even for quick’n’dirty cmdline tools one would want a proper declarative arg parser with help texts etc…).

5 Likes

And the arg parser is something planned for anyway (including for getting your options parsed as an optional additional parameter.

3 Likes

Possibly env var handling as well.

There are several simple libs now that provide a .env loader that provide extra access functions to locally override env vars. Probably doesn’t belong in init config, but it’s a common enough pattern. Easy enough to keep that in real main() of course

1 Like

I just realized something…

This is automagical Java-style dependency-injection :rofl:

4 Likes

No no no, Juice is definitely different from Guice.

6 Likes

I sure know I’ll be excited for 0.16 now! I primarily write CLI tools, so the whole std.Io story isn’t as exciting to me as it is others, but this is amaaaazing!

1 Like

Yes, and if you think about it, passing std.mem.Allocator is also kind of like that. So if you squint your eyes a bit, and do some comptime magic you can easily make it work with any function, like this:

And then, you can integrate this with some url routing, and auto-wrap handlers in inj.call(cx) and you basically have what tokamak was at the beginning.

And it turns out we can get really far (closer to real DI) in Zig, and it’s possible to define modules using structs, and then auto-generate all the plumbing in comptime too.

You can see that here, note that T.config(bundle)is still called during comptime, so you can import other modules and override providers and do whatever you want.

It’s still not what grown DICs are doing but it’s really close. Close enough to be useful.

BTW: I am not sure about that claim, maybe someone can help, but in theory, that inline for in CompiledBundle(ops) could be optimized-away by the compiler… In other words, the container setup itself could be the same speed as hand-written plumbing.

1 Like

I think std.Io will be very useful for CLI tools.

You may not have high concurrency requirements, but it also seemingly has nicer properties for process orchestration and the like since the process APIs were added to the vtable recently on master.

2 Likes