Using Zig as a C compiler

23-08-2024



Introduction

This post follows on from Testing C with Zig and digs a bit deeper into how you can use Zig as a C compiler. There are several advantages to this approach, the biggest for me being the simplicity of the Zig toolchain. Another bonus is of course that Zig can work seamlessly with Make files, just like your regular compiler. Out of interest, I'll also show how to use Task to do the same work as Make. I like Task a lot, it's a really handy tool. In my humble opinion, it's friendlier and easier to use than Make, but you do whatever works for you!

See Zig's Getting Started page for installation instructions. The rest of this article assumes you have Zig installed and that you have some C programming experience. Other than that, you don't need anything else, unless your system does not include Make! Then you might want to investigate getting that installed first, unless you plan on using Task instead, of course.

For convenience, the full source code this article is based on is available on Github here.


Setting up the C project

We will need to create quite a few C files for this project. I'm going to show you how I typically approach this task, but feel free to adjust things to your liking as we go along. First though, let's create a few things. All the file content is available in the Git repo, though I will show some snippets here.

  1. Create a new project directory, I called mine template
  2. Navigate into this new directory
  3. I created a LICENSE.txt file and a README.md file, but you don't need to.
  4. Create a Makefile file, and, optionally, a Taskfile.yml file.
  5. Create a src directory
  6. Navigate to the src directory.
  7. Create a main.c and tests.c files in the src directory.
  8. Create a lib directory under the src directory.
  9. Create my_lib.c, my_lib.h, my_math.c and my_math.h in the new lib directory.

When all is said and done, you should have a structure like this in the template directory:
(README, LICENSE and Taskfile files are optional)


For your convenience, some of the file content is listed here. Apply your own formatting preferences please. Also double check the spacing in the Makefile file, that can be a source of frustration.

Makefile

CC=zig cc
RUNNER=zig run
LICENSE_FILE=LICENSE.txt
README_FILE=README.md
SRC_FOLDER=src
MAIN_FILE=main.c
TEST_FILE=tests.c
INCLUDE_FILES=lib/my_lib.c lib/my_math.c
OUT_FOLDER=bin
OUT_FILE=output

.PHONY: default
default:
	@echo
	@echo "Make file to use Zig as a drop-in C compiler."
	@echo
	@echo "Options:"
	@echo "default: Displays make options."
	@echo "license: Displays the license file."
	@echo "readme:  Displays the readme file."
	@echo "run:     Runs the main.c file."
	@echo "build:   Builds the binaries to the bin directory."
	@echo "test:    Runs the project tests."
	@echo

.PHONY: license
license:
	cat $(LICENSE_FILE)

.PHONY: readme
readme:
	cat $(README_FILE)

.PHONY: run
run:
	@cd $(SRC_FOLDER);$(RUNNER) $(MAIN_FILE) $(INCLUDE_FILES);cd ..;

.PHONY: test
test:
	@clear;cd $(SRC_FOLDER);$(RUNNER) $(TEST_FILE) $(INCLUDE_FILES);cd ..;

.PHONY: build
build:
	@mkdir -p $(OUT_FOLDER);cd $(SRC_FOLDER);$(CC) $(MAIN_FILE) $(INCLUDE_FILES) -o ../$(OUT_FOLDER)/$(OUT_FILE);cd ..;

            

Taskfile.yml


# https://taskfile.dev

version: "3"

vars:
  SRC_FOLDER: src
  LICENSE_FILE: LICENSE.txt
  README_FILE: README.md
  MAIN_FILE: main.c
  TEST_FILE: tests.c
  INCLUDE_FILES: lib/my_lib.c lib/my_math.c
  OUT_FOLDER: bin
  OUT_FILE: output

tasks:
  default:
    desc: Lists available tasks.
    cmds:
      - task --list-all
    silent: true
  license:
    desc: Displays the license.
    cmds:
      - cat {{.LICENSE_FILE}}
    silent: true
  readme:
    desc: Displays the README file.
    cmds:
      - cat {{.README_FILE}}
    silent: true
  run:
    desc: Runs '{{.MAIN_FILE}}'.
    dir: "{{.SRC_FOLDER}}"
    cmds:
      - zig run {{.MAIN_FILE}} {{.INCLUDE_FILES}}
    silent: true
  build:
    desc: Builds the main binary to the '{{.OUT_FOLDER}}' folder.
    dir: "{{.SRC_FOLDER}}"
    cmds:
      - mkdir -p ../{{.OUT_FOLDER}}
      - zig cc {{.MAIN_FILE}} {{.INCLUDE_FILES}} -o '../{{.OUT_FOLDER}}/{{.OUT_FILE}}'
    silent: true
  test:
    desc: Runs the C-based tests.
    dir: "{{.SRC_FOLDER}}"
    cmds:
      - clear
      - zig run {{.TEST_FILE}} {{.INCLUDE_FILES}}
    silent: true

            

main.c


#include "lib/my_lib.h"
#include "lib/my_math.h"

#include <stdio.h>
#include <stdlib.h>

int main() {
    say_hello();

    int one = 10, two = 20;
    printf("The larger of %d and %d is clearly %d.\n", one, two, biggestOf(one, two));
    
    return EXIT_SUCCESS;
}  
            

tests.c


#include <stdio.h>
#include <stdlib.h>

#include "lib/my_math.h"

#define ENSURE(expr)                                                           \
  if (expr) {                                                                  \
    printf("\033[0;32mPASS: %s\n\033[0m", #expr);                              \
  } else {                                                                     \
    printf("\033[0;31mFAIL: %s\n\033[0m", #expr);                              \
  }

void display_header(void);
void display_footer(void);

void test_biggestOf(void) {
  puts("Testing biggest_Of");

  int left = 10, right = 8;

  int biggest = biggestOf(left, right);
  ENSURE(biggest == 10);

  biggest = biggestOf(right, left);
  ENSURE(biggest == 10);

  left = 7;
  biggest = biggestOf(right, left);
  ENSURE(biggest == 8);

  biggest = biggestOf(left, right);
  ENSURE(biggest == 8);
}

int main() {
  display_header();

  test_biggestOf();

  display_footer();
  return EXIT_SUCCESS;
}

void display_header(void) {
  puts("\nStarting tests.\n------------------------------\n");
}

void display_footer(void) {
  puts("\n______________________________\nCompleted tests.\n");
}
            

my_lib.c


#include <stdio.h>

void say_hello(void) { 
    puts("Just a quick hello!"); 
}
            

my_lib.h


#ifndef __MY_LIB_H__
#define __MY_LIB_H__

void say_hello(void);

#endif            
            

my_math.c


int biggestOf(int left, int right) {
    return (left > right)? left:right;
}            
            

my_math.h


#ifndef __MY_MATH_H_
#define __MY_MATH_H_

int biggestOf(int left, int right);

#endif
            


You should now be able to run it:


    make run
    or
    task run
            

On my system, I see:


    Just a quick hello!
    The larger of 10 and 20 is clearly 20.
            

So let's run the tests:


    make test
    or
    task test
            

On my system, I see:


Starting tests.
------------------------------

Testing biggest_Of
PASS: biggest == 10
PASS: biggest == 10
PASS: biggest == 8
PASS: biggest == 8

______________________________
Completed tests.            
            

Alright, now open up the project in your favourite IDE or editor and take a good look at the files. You will notice that it's just standard C code, no Zig in sight! Sure, the Makefile and Taskfile.yml mentions the Zig compiler, but other than that, it's pretty much what you'd expect for a C project.

Take a closer look at the run option in the Make or Task file. See something interesting? Zig allows you to build and run C code with just a single command - run! Super handy! In fact, you can easily run single C source files this way as well: zig run hello_world.c will build and execute the file in the terminal. Super stuff when you are learning or experimenting with C.

Speaking of editors, have you checked out Zed yet? It's definitely worth a visit, it's quickly becoming one of my favourite tools.


Conclusion

Using Zig as a C compiler is so simple, easy and convenient. I highly recommend considering if, especially if you are just getting started and the compiler thing is a bit overwhelming. The ability to just run C code with Zig makes it a lot easier to focus on learning C and not get bogged down in tooling installation issues.


Resources

To learn more about Zig or C, you can use the following resources as a starting point:



Return to the blog index