Zig NEWS

Cover image for Growing a {{mustache}} with Zig
Rafael Batiati
Rafael Batiati

Posted on

Growing a {{mustache}} with Zig

Mustache is a well-known templating system used to render dynamic documents, a popular tool across many programming languages, such as Ruby, JavaScript, Go, Rust, C#, Java, Lua, and now Zig!

For more details about Mustache, please visit https://mustache.github.io/

Introducing mustache-zig

As a Zig enthusiast, I'm looking forward to building something with Zig that I could put into production one day, and this project started out as a yak-shaving of my own needs but also aims to be a general-purpose and fully compliant mustache implementation.

For more details about this project, please visit
https://github.com/batiati/mustache-zig

Mustache templates

With more spin-offs than the Star Trek™ franchise, Mustache has numerous implementations. While some of them follow the specification, others (like Handlebars for example) introduce and remove features, often being considered as a new "mustache-like" templating system.

The best thing I can say about Mustache templates is their "logic-less" aspect. It allows you to control everything from the data source, leaving the template unaware of conditions and control flows. It's easy to write templates, and it's easier to maintain the business logic behind them. No hidden control flow, sounds familiar to Zig, isn't it?

Example of a template:

Invoice: {{invoice_number}}
Date: {{date}}
{{#items}}
{{name}}......{{value}}
{{/items}}
{{#paid}}
PAID
{{/paid}}
{{^paid}}
NON PAID
{{/paid}}
Enter fullscreen mode Exit fullscreen mode

You can interpolate values, iterate over loops and conditionally render something.

pub fn main() anyerror!void {
    const my_template_text =
        \\Invoice: {{invoice_number}}
        \\Date: {{date}}
        \\{{#items}}
        \\{{name}}......{{value}}
        \\{{/items}}
        \\{{#paid}}
        \\PAID
        \\{{/paid}}
        \\{{^paid}}
        \\NON PAID
        \\{{/paid}}
    ;

    const my_struct = .{
        .invoice_number = 2022100,
        .date = "08-13-2022",
        .items = .{
            .{ .name = "Movement #1", .value = 120.0 },
            .{ .name = "Movement #2", .value = 45.20 },
            .{ .name = "Movement #3", .value = 99.99 },
        },
        .paid = false,
    };

    var allocator = std.heap.page_allocator;
    const rendered = try mustache.allocRenderText(allocator, my_template_text, my_struct);
    defer allocator.free(rendered);

    var out = std.io.getStdOut();
    try out.writeAll(rendered);
}
Enter fullscreen mode Exit fullscreen mode

Output:
Image description

Despite being "logic-less", Mustache templates are quite powerful, allowing you to combine multiple templates and even implement custom logic with your own functions through lambda expansion.

For more details about all features, please refer to the Mustache manual.

Creating robust, optimal, and reusable software with Zig

Well, I don't know if I can say those things about mustache-zig, but at least those were my goals, and I can assure you that Zig did a good job motivating me to do so.

Robust isn't something you can say about software until it has been
battle-tested in the real world, but from a theoretical standpoint, there is over 96% of test coverage in the codebase.
All tests were implemented easily during development (TDD is a big deal for me) thanks to a neat built-in test block, allowing me to test everything (private, public, unit, and integrated) more naturally from where it makes the most sense, no testing frameworks, no special files other than the code, just plain Zig. Everything gets tested, including memory leaks and segfaults.

Optimal was my concern, but it wasn't an obsession: I'd say that between "good enough" and "blazing fast", mustache-zig performs decently compared to other implementations. Out of curiosity, the excellent ramhorns project has some nice benchmarks comparing popular Rust templating systems, which I took as a baseline to mustache-zig benchmarks results (please, I don't want to start a drag racing here!).
The fact is, I have no previous experience writing high-performance software. Even though there are still plenty of opportunities to optimize the code, Zig itself guided me to think about efficiency, avoiding unnecessary heap allocations and using intrusive data structures, no premature optimizations, just the way the code gets better.

Reusable was my major concern, I wanted to create a piece of software that could be fine-tuned to perform well in a wide range of use cases. The user can trade between better performance by caching templates in memory (or even declaring them as comptime) or minimal memory footprint by rendering templates from streams without loading or storing the full content. Some features such as lambdas and custom delimiters can be turned off with comptime options, reducing the amount of code compiled.
A great feature of the Zig language is that it allows the public API to be designed in a way that looks very simple (no complicated templates <T>, traits, or constraints), but actually does a great deal of generic specialization and comptime validations when used with different types. I must confess, I wasn't a big fan of using anytype parameters, but I started to appreciate them because of all the complexity they can hide from the end-user. Combined with good documentation, it's a big win for both library authors and consumers.

My use-case ... Incrementally improve with Zig

In my use-case, I have an endpoint that generates dynamic documents from templates (stuff like tickets, contracts, service agreements, etc).
Those templates are all user-defined and usually are just few KB in size, but occasionally can reach larger sizes (as in a case where the customer embedded several images encoded as Base64 for an e-mail body).

Loading and rendering such large templates severely impacts memory consumption, especially due to a lot of strings being duplicated during the process (you know, it's quite common for GCed languages ​​to copy when a substring is obtained).

I don't have much choice other than:
a) Buy more memory for my servers 💸.
b) Spend a large amount of time building some questionable cache based on the most used templates 🥱.
c) Spend an even larger amount of time building a new template engine optimized for my specific use 🤯.

Of course, I did choose the option "a", it is what everyone does, isn't it? But why not spend an insane amount of spare-time building some cool stuff in Zig? 🥰

So, here we are, we have a shippable library, a valid use case, and an opportunity to cut costs down, but we can't afford to take unnecessary risks. As much as I want to get my code into production, the Zig ecosystem is still immature to develop and run a production-grade service.

Foreign Function Interface

This wasn't the plan, but as soon as I realized this option, I started an FFI PoC with very promising results.

Mustache-zig exposes simple FFI functions that expect some function pointers as argument. Those function pointers are called during the rendering process, allowing the caller to directly write its variables to the output buffer when it is needed, without unnecessary copies.

This approach allowed me to render any template from my existing Dotnet project using just plain C# classes, getting advantage of everything else that was already implemented in mustache-zig. Pretty exciting!

For more information about the mustache-zig FFI, please see the C example and the C# example

Conclusion

This post is not a technical article, it's mostly about my own journey as a software developer investing my time learning and discovering the benefits of Zig to the point that I even wanted to use it in production on a professional project.

There are a lot of good libraries out there, and I'm sure if I took the time, I would find many good options within the Dotnet ecosystem to suit my requirements.
I also don't want to imply that the X or Y language or library is inefficient/slow, for example, the excellent Stubble.net project manages to emit compiled code, rendering templates with performance even superior to many native implementations.

Mustache-zig is still not finished yet, I still have a lot of work to do. I solely hope this post can inspire others to take a closer look at Zig as a real alternative for the future.

Discussion (1)

Collapse
kristoff profile image
Loris Cro

Thank you for the writeup! I really appreciate how you approached the development from a very holistic point of view: performance, reusability, and interoperability with other languages.

I think this is the right mindset when writing a library in Zig.