0% found this document useful (0 votes)
2 views5 pages

Debugging Techniques for Zig Apps

This document provides an overview of debugging techniques for Zig applications, emphasizing print debugging and the use of debuggers like GDB and LLDB. It details how to implement print statements for visibility into program behavior and explains the steps to compile code in debug mode for effective debugging. Additionally, it covers investigating data types of objects using both LLDB commands and Zig's built-in @TypeOf() function.

Uploaded by

samithegreat111
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views5 pages

Debugging Techniques for Zig Apps

This document provides an overview of debugging techniques for Zig applications, emphasizing print debugging and the use of debuggers like GDB and LLDB. It details how to implement print statements for visibility into program behavior and explains the steps to compile code in debug mode for effective debugging. Additionally, it covers investigating data types of objects using both LLDB commands and Zig's built-in @TypeOf() function.

Uploaded by

samithegreat111
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

1.

5 Debugging Zig applications

Introduction to Zig

Welcome
1 Introducing Zig
2 Control flow, structs, modules and types
3 Memory and Allocators
4 Project 1 - Building a base64 encoder/decoder
5 Debugging Zig applications
6 Pointers and Optionals
7 Project 2 - Building a HTTP Server from scratch
8 Unit tests
9 Build System
10 Error handling and unions
11 Data Structures
12 Project 3 - Building a stack data structure
13 Filesystem and Input/Output (IO)
14 Zig interoperability with C
15 Project 4 - Developing an image filter
16 Introducing threads and parallelism in Zig
17 Introducing Vectors and SIMD
References

Table of contents
5.1 Print debugging
5.2 Debugging through debuggers
5.2.1 Compile your source code in debug mode
5.2.2 Let’s debug a program
5.3 How to investigate the data type of your objects

5 Debugging Zig applications


Being able to debug your applications is essential for any programmer who wants to do serious programming in any
language. That is why, in this chapter, we are going to talk about the available strategies and tools to debug applications
written in Zig.

5.1 Print debugging


We begin with the classic and battle-tested print debugging strategy. The key advantage that debugging offers you is
visibility. With print statements you can easily see what results and objects are being produced by your application.

This is the essence of print debugging - using print expressions to see the values that are being generated by your program,
and, as a result, get a much better understanding of how your program is behaving.

Many programmers often resort to the print functions in Zig, such as the [Link](), or, the [Link](), to
get a better understanding of their programs. This is a known and old strategy that is very simple and effective, and it’s
better known within the programming community as print debugging. In Zig, you can print information to the stdout or
stderr streams of your system.

Let’s begin with stdout. First, you need to get access to the stdout, by calling the getStdOut() method, from the Zig
Standard Library. This method returns a file descriptor object, and, through this object you can read/write to the stdout. I
recommend you to check out all methods available in this object, by checking the page in the Zig Standard Library Official
Reference for the type File1.

For our purpose here, which is to write something to the stdout, especially to debug our program, I recommend you to use
the writer() method, which gives you a writer object. This writer object offers some helper methods to write stuff into the
file descriptor object that represents the stdout stream. In special, the print() method.

The print() method from this writer object is a “print formatter” type of a function. In other words, this method works
exactly like the printf() function from C, or, like println!() from Rust. In the first argument of the function, you specify
a template string, and, in the second argument, you provide a list of values (or objects) that you want to insert into your
template message.

Ideally, the template string in the first argument should contain some format specifier. Each format specifier is matched to a
value (or object) that you have listed in the second argument. So, if you provided 5 different objects in the second argument,
then, the template string should contain 5 format specifiers, one for each object provided.

Each format specifier is represented by a single letter, and you provide this format specifier inside a pair of curly braces. So,
if you want to format your object using the string specifier (s), then, you can insert the text {s} in your template string. Here
is a quick list of the most used format specifiers:

d: for printing integers and floating-point numbers.


c: for printing characters.
s: for printing strings.
p: for printing memory addresses.
x: for printing hexadecimal values.
any: use any compatible format specifier (i.e., it automatically selects a format specifier for you).

The code example below gives you an example of use of this print() method with the d format specifier.

const std = @import("std");


const stdout = [Link]().writer();
fn add(x: u8, y: u8) u8 {
return x + y;
}

pub fn main() !void {


const result = add(34, 16);
try [Link]("Result: {d}", .{result});
}

Result: 50

It’s important to emphasize that, the [Link]() method, as you would expect, prints your template string into the
stdout stream of your system. However, you can also print your template string into the stderr stream if your prefer. All
you need to do, is to replace the [Link]() call with the function [Link](). Like this:
const std = @import("std");
fn add(x: u8, y: u8) u8 {
return x + y;
}

pub fn main() !void {


const result = add(34, 16);
[Link]("Result: {d}\n", .{result});
}

Result: 50

You could also achieve the exact same result by getting a file descriptor object to stderr, then, creating a writer object to
stderr, then, using the print() method of this writer object, like in the example below:
const std = @import("std");
const stderr = [Link]().writer();
// some more lines ...
try [Link]("Result: {d}", .{result});

5.2 Debugging through debuggers


Although print debugging is a valid and very useful strategy, most programmers prefer to use a debugger to debug their
programs. Since Zig is a low-level language, you can use either GDB (GNU Debugger), or LLDB (LLVM Project
Debugger) as your debugger.

Both debuggers can work with Zig code, and it’s a matter of taste here. You choose the debugger of your preference, and you
work with it. In this book, I will use LLDB as my debugger in the examples.

5.2.1 Compile your source code in debug mode


In order to debug your program through a debugger, you must compile your source code in Debug mode. Because when you
compile your source code in other modes (such as Release), the compiler usually strips out some essential information that
is used by the debugger to read and track your program, like PDB (Program Database) files.

By compiling your source code in Debug mode, you ensure that the debugger will find the necessary information in your
program to debug it. By default, the compiler uses the Debug mode when compiling your code. Having this in mind, when
you compile your program with the build-exe command (which was described in Section 1.2.4), if you don’t specify an
explicit mode through the -O command-line 2 argument, then, the compiler will compile your code in Debug mode.

5.2.2 Let’s debug a program

As an example, let’s use LLDB to navigate and investigate the following piece of Zig code:
const std = @import("std");
const stdout = [Link]().writer();

fn add_and_increment(a: u8, b: u8) u8 {


const sum = a + b;
const incremented = sum + 1;
return incremented;
}

pub fn main() !void {


var n = add_and_increment(2, 3);
n = add_and_increment(n, n);
try [Link]("Result: {d}!\n", .{n});
}

Result: 13!

There is nothing wrong with this program. But it’s a good start for us. First, we need to compile this program with the zig
build-exe command. For this example, suppose that I have compiled the above Zig code into a binary executable called
add_program.

zig build-exe add_program.zig

Now, we can start LLDB with add_program, like this:

lldb add_program

From now on, LLDB is started, and you can know that I’m executing LLDB commands by looking at the prefix (lldb). If
something is prefixed with (lldb), then you know that it’s a LLDB command.
The first thing I will do, is to set a breakpoint at the main() function, by executing b main. After that, I just start the
execution of the program with run. You can see in the output below, that the execution stopped at the first line in the
function main(), as we expected.
(lldb) b main
Breakpoint 1: where = debugging`[Link] + 22
at [Link]:30, address = 0x00000000010341a6
(lldb) run
Process 8654 launched: 'add_program' (x86_64)
Process 8654 stopped
* thread #1, name = 'add_program',
stop reason = breakpoint 1.1 frame #0: 0x10341a6
add_program`[Link] at add_program.zig:11:30
8 }
9
10 pub fn main() !void {
-> 11 var n = add_and_increment(2, 3);
12 n = add_and_increment(n, n);
13 try [Link]("Result: {d}!\n", .{n});
14 }

I can start navigating through the code, and checking the objects that are being generated. If you are not familiar with the
commands available in LLDB, I recommend you to read the official documentation of the project3. You can also look for
cheat sheets, which quickly describes all commands available for you4.

Currently, we are in the first line at the main() function. In this line, we create the n object, by executing the
add_and_increment() function. To execute the current line of code, and go to the next line, we can run the n LLDB
command. Let’s execute this command.

After we executed this line, we can also look at the value stored inside this n object by using the p LLDB command. The
syntax for this command is p <name-of-object>.

If we take a look at the value stored in the n object (p n), notice that it stores the hexadecimal value 0x06, which is the
number 6 in decimal. We can also see that this value has a type of unsigned char, which is an unsigned 8-bit integer. We
have talked already about this in Section 1.8, that u8 integers in Zig are equivalent to the C data type unsigned char.

(lldb) n
Process 4798 stopped
* thread #1, name = 'debugging',
stop reason = step over frame #0: 0x10341ae
debugging`[Link] at [Link]:26
9
10 pub fn main() !void {
11 var n = add_and_increment(2, 3);
-> 12 n = add_and_increment(n, n);
13 try [Link]("Result: {d}!\n", .{n});
14 }
(lldb) p n
(unsigned char) $1 = '\x06'

Now, on the next line of code, we are executing the add_and_increment() function once again. Why not step inside this
function? Shall we? We can do that, by executing the s LLDB command. Notice in the example below that, after executing
this command, we have entered into the context of the add_and_increment() function.

Also notice in the example below that, I have walked two more lines in the function’s body, then, I execute the frame
variable LLDB command, to see at once, the value stored in each of the variables that were created inside the current
scope.

You can see in the output below that, the object sum stores the value \f, which represents the form feed character. This
character in the ASCII table, corresponds to the hexadecimal value 0x0C, or, in decimal, the number 12. So, this means that
the result of the expression a + b executed at line 5, resulted in the number 12.
(lldb) s
Process 4798 stopped
* thread #1, name = 'debugging',
stop reason = step in frame #0: 0x10342de
debugging`debug1.add_and_increment(a='\x02', b='\x03')
at [Link]:39
-> 4 fn add_and_increment(a: u8, b: u8) u8 {
5 const sum = a + b;
6 const incremented = sum + 1;
7 return incremented;
(lldb) n
(lldb) n
(lldb) frame variable
(unsigned char) a = '\x06'
(unsigned char) b = '\x06'
(unsigned char) sum = '\f'
(unsigned char) incremented = '\x06'

5.3 How to investigate the data type of your objects


Since Zig is a strongly-typed language, the data types associated with your objects are very important for your program. So,
debugging the data types associated with your objects might be important to understand bugs and errors in your program.

When you walk through your program with a debugger, you can inspect the types of your objects by simply printing them to
the console, with the LLDB p command. But you also have alternatives embedded in the language itself to access the data
types of your objects.

In Zig, you can retrieve the data type of an object, by using the built-in function @TypeOf(). Just apply this function over the
object, and you get access to the data type of the object.

const std = @import("std");


const stdout = [Link]().writer();
const expect = [Link];

pub fn main() !void {


const number: i32 = 5;
try expect(@TypeOf(number) == i32);
try [Link]("{any}\n", .{@TypeOf(number)});
}

i32

This function is similar to the type() built-in function from Python, or, the typeof operator in Javascript.

1. [Link]

2. See [Link]

3. [Link]

4. [Link]
4 Project 1 - Building a base64 encoder/decoder
6 Pointers and Optionals

You might also like