Skip to content

juicy main #24510

Closed
Closed

Description

@andrewrk

What if you could receive a bunch of goodies in main simply by asking for them?

pub fn main(init: std.process.Init, args: Args) anyerror!void { ... }

The first arg:

pub const Init = struct {
    /// Permanent storage for the entire process, cleaned automatically on exit.
    arena: *std.heap.ArenaAllocator,
    /// A default-selected general purpose allocator for temporary heap allocations.
    /// Debug mode will set up leak checking.
    gpa: std.mem.Allocator,
    /// An appropriate default Io implementation based on the target configuration.
    /// Debug mode will set up leak checking.
    io: std.Io,
    /// Environment variables.
    environ: std.process.Environ,
    /// Os-specific stuff.
    os: union {
        linux: Aux,
        windows: Peb,
        // ...
    },
};

The second arg is user-defined is does that CLI pattern where you provide a type that contains already parsed CLI args, and then the start code uses type reflection to parse the args before calling main:

const Args = struct {
    named: struct {
        example: u32,
    },
    /// Memory allocated into Init.arena
    positional: []const []const u8,
};

This would be a minimal, opinionated parser that users would opt out of the moment they want to do something remotely sophisticated, by accepting []const []const u8 instead of a struct.

If you think about it we already have a little bit of this with the return type - if it returns an error, start code does some opinionated stuff when an error is returned, such as print an error return trace (debug) or the error name (release) and exit(1).

Users who want full control over everything can continue to use pub fn main() noreturn {} and have the start code do almost nothing (beyond setting up Thread Local Storage, signal handling, PIE, etc., as it already does).

Activity

  1. added this to the upcoming milestone on Jul 19, 2025
  2. added
    enhancementSolving this issue will likely involve adding new logic or components to the codebase.
    standard libraryThis issue involves writing Zig code for the standard library.
    proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.
    on Jul 19, 2025
  3. MasonRemaley commented on Jul 19, 2025

    @MasonRemaley
    Contributor

    Thinking out loud--

    Returning errors from main is useful because it allows you to use "try" in main. Without this feature, people would either recreate it manually or just write worse code, so it makes sense that we support it.

    I think my argument in favor of something like juicyMain is similar, though it's not as strong.

    Take argument parsing as an example since it's one of the things juicyMain would provide. Argument parsing isn't hard, but it's still work to get right.

    If basic argument parsing is provided out of the box, the quality of the average Zig executable likely increases because, well, why not add a nice CLI to your exe if it's easy?

    This argument isn't as strong as my argument for returning errors from main though, since there are plenty of other ways to make argument parsing easy without juicyMain (a std api, a third party package, etc.)

    Still, I think the convenience of this proposal probably outweighs the minimal increase in complexity.

  4. MasonRemaley commented on Jul 19, 2025

    @MasonRemaley
    Contributor

    Side note--I've been using an argument parser that works similarly to what's described here in all my Zig projects.

    I don't want to derail this thread w/ argument parsing details, but if/when it's time to sort that part of things out I can share some insights or help design it.

  5. InKryption commented on Jul 19, 2025

    @InKryption
    Contributor

    Question: would the bare main be in position to capture argv and other on-startup data?
    Also, will untyped argv be allocated by the arena as well?

  6. BeetMacol commented on Jul 20, 2025

    @BeetMacol

    Would other combinations be allowed? Simple logic could allow anything, including:

    pub fn main(init: std.process.Init, args: Args) anyerror!void { ... }
    pub fn main(args: Args, init: std.process.Init) anyerror!void { ... }
    pub fn main(init: std.process.Init, args: Args) void { ... }
    pub fn main(init: std.process.Init) anyerror!void { ... }
    pub fn main(args: Args) anyerror!void { ... }
    pub fn main(args: []const u8) anyerror!void { ... }

    Also, are the names init and args required, or can they be freely changed?

  7. nickelca commented on Jul 20, 2025

    @nickelca

    Zig does not have reflection on parameter names, so no, the names aren't required

  8. castholm commented on Jul 20, 2025

    @castholm
    Contributor

    Resource management-wise, how do you envision that command-line arguments and environment variables will be handled on operating systems like Windows, where they are passed to the process as WTF-16LE strings and need to be parsed and converted WTF-8?
    Will the startup code parse and allocate converted copies of them using the default allocator before calling the user's main function, accepting that precise control over memory management is taken away from the user?
    Or will you try to find a design for this hypothetical std.process.Init API that takes these platform differences into consideration, by e.g. exposing the args through member functions like init.allocArgs(allocator) so that the user retains full control over the memory management strategy?

  9. paperdev-code commented on Jul 20, 2025

    @paperdev-code

    I think it's great to finally have an argument parser provided by the std namespace.

    The second arg is user-defined is does that CLI pattern where you provide a type that contains already parsed CLI args

    Alternatively, we could have a system similar to clap, where the parameters are parsed at comptime from the help message. Through std_options.init_params ? I think a way to print a help message is required.

    Also, some way to integrate this with zig build's Step.Run would be cool. If we have a standard way to provide arguments, then perhaps we could provide them as a struct from inside the build script as well.

  10. scottredig commented on Jul 20, 2025

    @scottredig
    Contributor

    2¢: I think this would make more sense as a template (zig init) than as the standard main functionality. I don't think this is likely to give the optimal implementation for significant programs, so this creates a larger jump where you either do things sub-optimally or you have to copy a significant portion of the functionality anyways. With a template it's easy to trim what you don't need (eg, delete the gpa code if you don't need it), and modify what isn't quite right (eg, argument parsing is likely to need specific work to display good errors to a program's user).

  11. mrjbq7 commented on Jul 21, 2025

    @mrjbq7
    Contributor

    I think you could get there with some useful defaults modules:

    pub fn main() !void {
        var init: std.Init = .initDefault();
        defer init.deinit();
    
        var args = std.args.parse(init.allocator, Args);
        defer args.deinit(); 
    
        // do stuff...
    }
  12. matklad commented on Jul 21, 2025

    @matklad
    Contributor

    I am slightly apprehensive about arena in particular, because usually arena is scoped to something (frame/request), or you use append-only arena instead your GPA. There's more than one reasonable semantics for arena, and blessing one of them disadvantages others.

    The rest look fine to me. Maybe this also needs log: Log? Not sure how logging works in post-iopocalypse world. Similarly, could have stdin/stdout/stderr writers/readers set-up. Would solve the problem of tests liking to written stderr, if they don't have stderr.

    TigerBeetle flags.zig module might be an inspiration for Args parser, it's opinionated, small, and optimizes for low-friction when defining flags.

  13. silversquirl commented on Jul 21, 2025

    @silversquirl
    Contributor

    @matklad arena holds args and env vars. Since it's needed by std.start, might as well let main use it too!
    Strictly speaking, args and envs could be allocated in the gpa, but since it's a lot of small allocations, an arena is better.

    Arenas are also very useful for short-running programs, or for allocating things that are needed for the entire duration of the program run (eg. a config file). I think it's useful enough to be exposed, especially considering the alternative would often result in both std.start and user code immediately creating their own arena.

  14. 29 remaining items

  15. KilianHanich commented on Jul 31, 2025

    @KilianHanich

    Yes, just that this is being kept in mind: being able to use a Debugger on functions which were treated with Release optimisations.

  16. MarvinHannott commented on Aug 30, 2025

    @MarvinHannott

    Maybe I'm just thick, but this sounds like quite an unnecessary feature that doesn't really add anything but confusion, just to avoid 3 lines of code.

  17. ityonemo commented on Oct 12, 2025

    @ityonemo
    SponsorContributor

    This is so good that at some point in the past I had hallucinated in an exchange with @kristoff-it that something similar (more simple, only args iirc) already existed. He corrected me. Happy to see it happening.

  18. eamonburns commented on Oct 22, 2025

    @eamonburns
    Contributor

    It looks like #24510 (comment) is a pretty popular solution, but there hasn't been a whole lot of discussion around it.

    Is there any reason why having the "magic" arguments to main is better than explicitly initializing a struct that contains all the things you need?

    I think:

    1. This is better than the current status quo because you only need to initialize init instead of I/O, an allocator, an arena, the environment, etc.
    2. This is better than "magic" arguments because you know exactly what is happening, and it gives the opportunity to customize it a little bit. For instance, change the number of threads.
      • Simplest way would be const init: std.Init = .initDefault();
      • A little more advaced: const init: std.Init = .initConfig(.{ thread_cound = 5 });
      • .initDefault() would probably be exactly equivalent to .initConfig(.{}), and it might not even be needed
    3. This would allow for less of a leap from the easy way to the more advanced way:
      • Magic args:
        • Easy way: Everything is done for me
        • Advanced way: I have to do everything
      • Explicit initialization:
        • Easy way: I have to do a little initialization
        • Advanced way: I have to do all the initialization

    (This third point is mostly geared toward new users, but I think it can apply to more experienced users as well. Less difference between the "easy" and "advanced" code)

  19. castholm commented on Oct 24, 2025

    @castholm
    Contributor

    On platforms like Linux, command-line arguments, environment variables, auxiliary vectors and similar process arguments are only available as arguments passed to the entry point and can't be accessed any other way, so they need to be saved away to global variables or passed forward to parts of the program that need them. This is unlike e.g. Windows where process arguments are always available via the PEB and can be accessed by any parts of the process at any point in time.

    #4524 identified that global mutable variables like std.os.argv, std.os.environ, etc. is a huge footgun because they rely on startup code from std.start to initialize them. If you're building a library (static or dynamic) or otherwise not letting Zig be responsible for the entry point, these variables will remain undefined and APIs like std.process.argsAlloc() will happily crash your program if you're unaware of these details.

    So even in the basic "non-juicy" form, without any argument parsing or allocator/IO setup, main() still needs at least one argument:

    pub fn main(init: std.process.Init) !void { ... }
    pub const Init =
        if (os == .linux)
            struct {
                argv: [][*:0]u8,
                envp: [][*:0]u8, // #4524 suggests a different approach for envp/auxv
                auxv: []std.elf.Auxv,
            }
        else if (os == ...)
            struct {
                ...
            }
        else // Windows, etc. don't need any of this
            struct {};

    I don't have any strong opinions on whether the "juicy" setup/teardown of allocators, interfaces, arguments should be automatically handled by std.start or explicitly by the user with something like .initDefault(). The thing that's important that it should still be possible to define a main() function that receives only the basic entry point arguments, without any unnecessary setup or memory allocation.

  20. bradcypert commented on Nov 18, 2025

    @bradcypert

    I don't have many thoughts on implementation details for this, but I do think having this feature would help allow users to delegate some commonly used boilerplate (arg parsing for example) to Zig itself and ultimately help with adoption. That being said, does this brush up against Zig's ethos of explicit-over-implicit?

  21. KilianHanich commented on Nov 18, 2025

    @KilianHanich

    does this brush up against Zig's ethos of explicit-over-implicit?

    About as much as using any other library.

    So yes, you lose some explicitness, but that also goes for every kind of abstraction. But abstraction (if done right) helps with Zig's "communicate intent precisely" ethos.

  22. andrewrk commented on Jan 1, 2026

    @andrewrk
    MemberAuthor
  23. modified the milestones: upcoming, 0.16.0 on Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThis proposal is planned.enhancementSolving this issue will likely involve adding new logic or components to the codebase.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.standard libraryThis issue involves writing Zig code for the standard library.

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions