Gnuplot in Zig

Since it is ideal for carrying out analyses in signal processing and you often want/need to display the signals graphically, I use Gnuplot as a C call. For me, this has the advantage of being able to share commands with colleagues without any complications and also shows the perfect integration of Zig and C.

Maybe some of you also need it, which is why I am sharing my Zig code here:

fn plot_data(data: []Complex(f32)) !void {
    const c = @cImport({
        @cInclude("stdio.h");
    });

    const gnuplot = c.popen("gnuplot -persistent", "w");
    defer _ = c.pclose(gnuplot);

    _ = c.fprintf(gnuplot, "set title 'Sequence - L: %d'\n", data.len);
    _ = c.fprintf(gnuplot, "set xlabel 'Index'\n");
    _ = c.fprintf(gnuplot, "set ylabel 'Value'\n");
    _ = c.fprintf(gnuplot, "set grid\n");
    _ = c.fprintf(gnuplot, "plot '-' with lines linecolor 6 notitle");

    // Send data
    for (data, 0..) |v, i| {
        _ = c.fprintf(gnuplot, "%d %f\n", i, v.re);
    }
    
     // End of data
    _ = c.fprintf(gnuplot, "e\n");
}

Example:
plot

10 Likes

It is nice.
Is there a specific reason for not using std.process.Child with .stdin_behavior = .Pipe?

We are in a project and it should be quick, so I didnā€™t try to implement it with ā€˜std.process.Childā€™. So no. If there is a simple solution for this, please let me know.

There is a deficiency. We must wait the process to exit.
It is not a problem for gnuplot, since there are plot commands for image files.

const std = @import("std");

fn plot_data(data: []const f32, allocator: std.mem.Allocator) !void {
    const argv = [_][]const u8{ "gnuplot", "-persistent" };
    var child = std.process.Child.init(&argv, allocator);
    child.stdin_behavior = .Pipe;
    try child.spawn();
    const writer = child.stdin.?.writer();
    try writer.print(
        \\set title 'Sequence - L: {d}'
        \\set xlabel 'Index'
        \\set ylabel 'Value'
        \\set grid
        \\plot '-' with lines linecolor 6 notitle
        \\
    , .{data.len});

    // Send data
    for (data, 0..) |v, i| {
        try writer.print("{d} {}\n", .{ i, v });
    }

    // End of data
    try writer.print("e\n", .{});
    _ = try child.wait();
}

pub fn main() !void {
    const data = [4]f32{ 0, 1, 0, -1 } ** 16;
    try plot_data(&data, std.heap.page_allocator);
}

gnuplot

Version that saves to a file:

const std = @import("std");

fn plot_data(filename: []const u8, data: []const f32, allocator: std.mem.Allocator) !void {
    const argv = [_][]const u8{"gnuplot"};
    var child = std.process.Child.init(&argv, allocator);
    child.stdin_behavior = .Pipe;
    try child.spawn();
    const writer = child.stdin.?.writer();
    try writer.print(
        \\set term png
        \\set output '{s}'
        \\set title 'Sequence - L: {d}'
        \\set xlabel 'Index'
        \\set ylabel 'Value'
        \\set grid
        \\plot '-' with lines linecolor 6 notitle
        \\
    , .{ filename, data.len });

    // Send data
    for (data, 0..) |v, i| {
        try writer.print("{d} {}\n", .{ i, v });
    }

    // End of data
    try writer.print("e\nexit\n", .{});
    _ = try child.wait();
}

pub fn main() !void {
    const data = [4]f32{ 0, 1, 0, -1 } ** 16;
    try plot_data("test.png", &data, std.heap.page_allocator);
}
2 Likes

Thanks a lot, works great.

This line is not necessary.

From Child.spawn:

On success must call kill or wait.

Unfortunately wait is necessary to release resources.
A deinit must be provided from Child to release resources without waiting or killing the process.

But Gnuplot can run as a daemon, so you donā€™t have to wait. On my system it works perfectly without this wait line. :slightly_smiling_face:

But you will leak userspace process and io streams handles/descriptors, thatā€™s why wait() or kill() is crucial. Few bytes leaked is not a big deal though :grin:
I wish for API to ā€œreleaseā€ child process without needing to wait or kill it.

The output of Valgrind says no memory leak:

==32479== Memcheck, a memory error detector
==32479== Copyright (C) 2002-2024, and GNU GPL\'d, by Julian Seward et al.
==32479== Using Valgrind-3.23.1.GIT and LibVEX; rerun with -h for copyright info
==32479== Command: ./plot_signal fft_tap.bin
==32479== 
==32479== 
==32479== HEAP SUMMARY:
==32479==     in use at exit: 0 bytes in 0 blocks
==32479==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==32479== 
==32479== All heap blocks were freed -- no leaks are possible
==32479== 
==32479== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

I am using a buffer allocator, so every memory will be freed after finishing the program. Or do I oversee something?

Thereā€™s no memory leak but handle/descriptor leak so you see nothing in valgrind. I have to say that this doesnā€™t matter if your program is a ā€œsingle-shotā€. Problems will only arise if you do spawn thing many times.

1 Like

This small example will quickly drain file descriptors:

const std = @import("std");
const heap = std.heap;
const process = std.process;
const io = std.io;
const posix = std.posix;

pub fn main() !void {
    var gpa = heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    var arena = heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();

    for (0..1_000_000) |_| {
        var child = process.Child.init(&.{ "echo", "hello world" }, arena.allocator());
        try child.spawn();
        _ = arena.reset(.retain_capacity);

        _ = posix.waitpid(child.id, 0);
    }

    _ = try io.getStdIn().reader().readByte();
}

Check number of descriptors opened: sudo lsof -p $(pidof test)

EDIT: made example more realworld-ish

2 Likes