Mastering C: Building a Project with Nix

5 minute read Published: 2024-06-14

Welcome, fellow C enthusiasts, to a whimsical journey through the world of library dependencies in C programming! This is the second post in the series "Mastering C with Effective C" series! Not at all suprising to me, I've yet to write anything at all from the book, and I'm still poking around with build systems. I've been learning to use nix as a build system, and some things are becoming clear when learning C, which has no built in package manager...

Today, we will dive into the exciting realm of building C programs with external libraries, and we’ll showcase how to do it using Nix, a modern build system that promises reproducibility and simplicity. But don't worry, we’ll also touch upon the classic Makefile approach for comparison. Let’s get started!

The code from this post can be found here

The Challenge of Library Dependencies in C

Before we embark on our adventure, let's acknowledge the challenge: managing external libraries in C. While modern languages often come with robust package managers, C developers often find themselves navigating the rugged terrain of manual downloads and builds. But fear not! With the power of Nix, we can transform this daunting task into a delightful experience.

Introducing Our Hero: The Simple JSON Parsing Example

To illustrate our journey, we'll create a C program called simple-parse-example. This program will demonstrate basic JSON parsing using the jansson library. Here’s what it will do:

The C Program: simple-parse-example

Here’s our simple C program using the jansson library:

// simple-parse-example.c
#include <stdio.h>
#include <jansson.h>

int main() {
    // Creating a JSON object
    json_t *object = json_object();
    json_object_set_new(object, "name", json_string("Candlehopper"));
    json_object_set_new(object, "level", json_integer(5));
    json_object_set_new(object, "score", json_integer(12345));

    // Serialize JSON object to string
    char *json_str = json_dumps(object, JSON_INDENT(2));
    if (!json_str) {
        fprintf(stderr, "Error serializing JSON object.\n");
        return 1;
    }
    printf("Serialized JSON:\n%s\n", json_str);

    // Free the serialized string
    free(json_str);

    // JSON string to parse
    const char *json_input = "{\"name\": \"Candlehopper\", \"level\": 5, \"score\": 12345}";

    // Parse JSON string
    json_error_t error;
    json_t *parsed_object = json_loads(json_input, 0, &error);
    if (!parsed_object) {
        fprintf(stderr, "Error parsing JSON string: %s\n", error.text);
        return 1;
    }

    // Extract values
    json_t *name = json_object_get(parsed_object, "name");
    json_t *level = json_object_get(parsed_object, "level");
    json_t *score = json_object_get(parsed_object, "score");

    if (json_is_string(name) && json_is_integer(level) && json_is_integer(score)) {
        printf("Parsed JSON:\n");
        printf("name: %s\n", json_string_value(name));
        printf("level: %lld\n", json_integer_value(level));
        printf("score: %lld\n", json_integer_value(score));
    } else {
        fprintf(stderr, "Error extracting values from JSON object.\n");
        json_decref(parsed_object);
        return 1;
    }

    // Decrement reference counts to free memory
    json_decref(object);
    json_decref(parsed_object);

    return 0;
}

The Classic Approach: Using Makefile

First, let’s explore how to build this program using a Makefile. Here’s a simple Makefile to compile simple-parse-example with the jansson library:

Makefile

# the compiler to use
CC = clang

# compiler flags:
#  -g    adds debugging information to the executable file
#  -Wall turns on most, but not all, compiler warnings
CFLAGS  = -g -Wall
  
# files to link:
LFLAGS = -ljansson
  
# the names to use for both the target source files, and the output files:
TARGETS = simple-parse-example
  
all: $(TARGETS)
  
simple-parse-example: simple-parse-example.c
	$(CC) $(CFLAGS) -o simple-parse-example simple-parse-example.c $(LFLAGS)

clean:
	rm -f $(TARGETS)

To build the project:

make

To clean up the build files:

make clean

Pros and Cons of the Makefile Approach

Pros:

Cons:

The Modern Approach: Using Nix

Now, let's switch gears and harness the power of Nix to build our project. Nix provides reproducibility and isolation, ensuring our builds are consistent across different environments.

Nix Setup

Here’s how you can set up a default.nix file to build simple-parse-example:

# default.nix
{ pkgs ? import <nixpkgs> {} }:

let
  jansson = pkgs.jansson;
in
pkgs.stdenv.mkDerivation {
  pname = "simple-parse-example";
  version = "1.0";

  src = ./.;

  nativeBuildInputs = [ pkgs.clang ];
  buildInputs = [ jansson ];

  buildPhase = ''
    clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp simple-parse-example $out/bin/
  '';

  meta = with pkgs.lib; {
    description = "A simple program using jansson";
    license = licenses.mit;
    maintainers = [ maintainers.yourname ];
    platforms = platforms.unix;
  };
}

Building with Nix

To build your project using Nix:

  1. Navigate to your project directory:

    cd myproject
    
  2. Build the project:

    nix-build
    
  3. Run the binary:

    ./result/bin/simple-parse-example
    

Pros and Cons of the Nix Approach

Pros:

Cons:

Exploring Other Build Tools

While Makefiles and Nix are fantastic tools, there are other build systems and package managers worth exploring:

  1. CMake: A cross-platform build system that automates the configuration process. Learn more.
  2. vcpkg: A C/C++ library manager from Microsoft. Learn more.
  3. Conan: A decentralized package manager for C/C++. Learn more.

Conclusion

In this quirky adventure, we’ve explored the classic Makefile approach and the modern Nix approach to building a C program with external library dependencies. We’ve seen how Nix can bring reproducibility and ease of use to the table, making it a fantastic choice for C developers looking to streamline their build processes.

Happy coding, and may your builds always be reproducible!