Motivation

Coming from somewhat more modern languages (and it’s hard to argue given C’s long lineage), I am used to having a baseline that makes me productive from the get-go. Be it Python’s venv with pip and pytest, or nodejs with npm, webpack and mocka… In a nutshell, having a package manager and a testing framework are 2 things I’d consider essential. With C you need to go one step further due to how dependcies are resolved - which you seldom have to think about otherwise. As someone who had shied away from C for many years, I felt it was time to see whether the landscape had changed enough to lower the barrier to entry.

I’d like to stress there are probably better ways to achieve similar goals - what follows is the outcome from my tinkering, and specific to Linux (which applies just as well if you’re using WSL within Windows, which is what I mostly use).

Package & dependency management

One thing I love about say Python, is the famous import antigravity. With C that’s a little harder - even if you have the libraries installed, you need the header files to be made available - and the two seldom come together.

Using your system’s package manager

Most libraries will have a pair of packages available - the actual library (the .so file - which stands for shared objects - similar to a DLL on Windows, and .a for the statically linked version),compiled for your kernel/architecture, along with a -dev counterpart that contains the headers you’ll need. Take libssl for instance:

libssl-dev/focal-updates,now 1.1.1f-1ubuntu2.4 amd64 [installed]
  Secure Sockets Layer toolkit - development files

libssl1.1/focal-updates,now 1.1.1f-1ubuntu2.4 amd64 [installed,automatic]
  Secure Sockets Layer toolkit - shared libraries

You’ll want to make sure the -dev package matches the version of the ones you have installed - though if you start with that, your package manager should grab the relevant compiled one automatically.

If the library isn’t available in your distribution’s default directory, don’t fear! Someone in the community will often make a package available and it’ll be a case of adding their repository to your apt.sources (or whatever package management you use).

“But where are they now?”

There’s no overarching standard of where header files will be located - they could be sprinkled throughout the filesystem. When that’s the case, find is your friend. But don’t be fooled into thinking that just because the library is called libssl, all you have to do is find libssl.h:

❯ find /usr -name "libssl*"
/usr/lib/x86_64-linux-gnu/pkgconfig/libssl.pc
/usr/lib/x86_64-linux-gnu/libssl.so.1.1
/usr/lib/x86_64-linux-gnu/libssl.a
/usr/lib/x86_64-linux-gnu/libssl.so
/usr/share/doc/libssl-dev
/usr/share/doc/libssl1.1

What I usually do is refer to the documenation itself - e.g. the libssl C API, which in this case indicates that one of the headers is called openssl/ssl.h. Armed with new information:

❯ find /usr -name ssl.h
/usr/include/openssl/ssl.h

That said, /usr/include is fairly safe bet in most cases :)

Using Conan

Conan is a C/C++ package/dependency management tool written in Python from the fine folks at JFrog.

I came quite late to the party and it seems Conan used to be a lot friendlier about hosting more open-source/hobbyist packages. Nevertheless, a number of key packages continue to be available on Conan Center and as we’ll see, its cmake generator makes life easy.

Installing Conan

A guide to installing Conan is available on the official site but in a nutshell, it’s a set of Python tools.

As with most modules nowadays, I prefer to set up a dedicated virtual environment for this (I’m going to assume you have Python 3.6 or later):

❯ python3 -m venv ~/conanvenv
❯ source ~/conanvenv/bin/activate
❯ pip install conan

And that’s pretty much it for the installing part!

Adding dependencies

Let’s say we want to use libcurl, the library counterpart of the ever so popular curl utility. We start by searching for it on our remote (specified with -r):

❯ conan search "libcurl/*" -r=conancenter
Existing package recipes:

libcurl/7.64.1
libcurl/7.66.0
libcurl/7.67.0
libcurl/7.68.0
libcurl/7.69.1
libcurl/7.70.0
libcurl/7.71.0
libcurl/7.71.1
libcurl/7.72.0
libcurl/7.73.0
libcurl/7.74.0
libcurl/7.75.0
libcurl/7.76.0
libcurl/7.77.0
libcurl/7.78.0

Picking the latest, you can then look through all the packages:

❯ conan search "libcurl/7.78.0@" -r=conancenter -q "arch=x86_64 AND os=Linux and compiler=gcc" -j search.results.json
❯ jq "." < search.results.json | head -n 30
{
  "error": false,
  "results": [
    {
      "remote": "conancenter",
      "items": [
        {
          "recipe": {
            "id": "libcurl/7.78.0"
          },
          "packages": [
            {
              "id": "049002579c003cd388d7af79171e951ae7e088d5",
              "options": {
                "shared": "True",
                "with_libpsl": "False",
                "with_ssl": "openssl",
                "with_ldap": "False",
                "with_largemaxwritesize": "False",
                "with_c_ares": "False",
                "with_zstd": "False",
                "with_zlib": "True",
                "with_libssh2": "False",
                "with_brotli": "False",
                "with_libidn": "False",
                "with_librtmp": "False",
                "with_nghttp2": "False"
              },
              "settings": {
                "os": "Linux",

The reason there are so many entries is because each has been compiled with slighly different options - e.g. some with openssl, some with zlib etc…

At that point, I really find it easier to tell conan what I need and let it figure it out. In your conanfile.txt at the root of your project, you can add:

[requires]
libcurl/7.78.0

[options]
libcurl:with_zlib=True
libcurl:with_ssl=openss

and run conan install .. in your build/ directory. Amongst the output, you should see that both the zlib and ssl dependencies were pulled by virtue of being, well, dependencies:

conanfile.txt: Installing package
Requirements
    libcurl/7.78.0 from 'conancenter' - Downloaded
    openssl/1.1.1k from 'conancenter' - Downloaded
    zlib/1.2.11 from 'conancenter' - Downloaded
Packages
    libcurl/7.78.0:539b44da7a736f055c2112b92bba7f29d6d3c644 - Download
    openssl/1.1.1k:6af9cc7cb931c5ad942174fd7838eb655717c709 - Download
    zlib/1.2.11:6af9cc7cb931c5ad942174fd7838eb655717c709 - Download

If you’re wondering where those end up, check out the ~/.conan directory:

❯ find ~/.conan -name curl.h
/home/axiomiety/.conan/data/libcurl/7.78.0/_/_/package/539b44da7a736f055c2112b92bba7f29d6d3c644/include/curl/curl.h

As far as dependencies go, you’re pretty much set. Make those available to your compiler is covered in the following section.

Getting this to compile

That’s actually harder than it sounds. At its most basic level, you can tell your compiler where all the dependencies are located - e.g. with gcc for non-standard locations (primarily anything outside of /usr/include), you can give it the -I/path/to/header flag (no space!), or with the C_INCLUDE_PATH environment variable.

That’s cumbersome in my eyes. That’s why we have things like make that take care of most of this. However maintaining a Makefile isn’t much of an improvement. There are a couple of tools that assist in that matter but the golden standard is probably cmake.

Conan can help you generate the files required by cmake, which will in turn generate the files required by make (yeah I found that confusing too). But before we go any further, let’s create a brand new project called “throwaway”:

❯ mkdir -p throwaway/{src,build,test}
❯ touch throwaway/conanfile.txt
❯ touch throwaway/CMakeLists.txt

Your conanfile.txt should look like this:

[generators]
cmake

We’ll add dependencies later - right now the focus is on getting an executable.

CMakeLists.txt is almost as simple:

cmake_minimum_required(VERSION 3.5)
project(throwaway)

add_definitions("-std=c11")

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

add_executable(main src/main.c)
target_link_libraries(main ${CONAN_LIBS})

Do a conan install .. in throwaway/build - this will generate bunch of files you don’t need to worry too much about (and being your build directory, you can delete this and start from scratch). If you’re interested, there will be a file called build/conanbuildinfo.cmake that contains all the secret sauce.

In the same throwaway/build directory, do a cmake .. which should result in:

CMake Error at CMakeLists.txt:9 (add_executable):
  Cannot find source file:

    src/main.c

  Tried extensions .c .C .c++ .cc .cpp .cxx .cu .m .M .mm .h .hh .h++ .hm
  .hpp .hxx .in .txx


CMake Error at CMakeLists.txt:9 (add_executable):
  No SOURCES given to target: main

Ha yes. The source code. Create throwaway/src/main.c with the following content:

#include <stdio.h>
  
int main(int argc, char *argv[]) {
    int answer = 42;
    printf("The meaning of life is indeed %d\n", answer);
}

Run cmake .. in your build directory again and watch in awe as more files are generated. The driver of all this is Makefile, which should start with something simliar to those 2 lines:

# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.16

Now you’re ready to compile! Still in throwaway/build, call make:

❯ make
Scanning dependencies of target main
[ 50%] Building C object CMakeFiles/main.dir/src/main.c.o
[100%] Linking C executable bin/main
[100%] Built target main
❯ ./bin/main
The meaning of life is indeed 42

Testing framework

Testing has become such an integral part of how I develop I feel naked without it (most times - perhaps a “do as I say, not as I do”). I have used a number of testing frameworks in probably as many languages, but when it came to C I drew a blank - I couldn’t even name one. So instead I headed over to Wikipedia and looked for something vaguely interesting. I came across cmocka which only seemed to be (1) actively maintained, (2) had a strong basis (cmockery from Google) and (3) minimal dependencies (again, that’s something you need to worry about and why dependency management is so handy).

Being relatively popular, there was already a package available for Ubuntu which had the latest build (1.1.5).

❯ apt search cmocka
Sorting... Done
Full Text Search... Done
cmocka-doc/focal,now 1.1.5-2 all [installed,automatic]
  documentation for the CMocka unit testing framework

libcmocka-dev/focal,now 1.1.5-2 amd64 [installed]
  development files for the CMocka unit testing framework

libcmocka0/focal,now 1.1.5-2 amd64 [installed]
  library for the CMocka unit testing framework

libpamtest0/focal 1.0.7-4build1 amd64
  Library to test PAM modules

libpamtest0-dev/focal 1.0.7-4build1 amd64
  Library to test PAM modules

It was a case of installing libcmocka0 along with its -dev counterpart, which provides the necessary header files.

The astute reader will notice I didn’t install this via Conan. Well, I couldn’t find it on Conan Center - and since it was available via my package manager, why struggle?

We’ll cover its usage in a bit more detail below.

Putting it all together

So at this point we have dependency management sorted, we have installed a testing framework via a system package. Let’s continue with our aptly named throwaway project!

Adding an external library

Let’s keep things consitent and say we want to use libcurl. Adding the dependency to throwaway/conanfile.txt means the file now looks like:

[generators]
cmake

[requires]
libcurl/7.78.0

[options]
libcurl:with_zlib=True
libcurl:with_ssl=openssl

Follow this by a conan install .. in throwaway/build. If you had already gone through the libcurl install above, you would see conan sourcing those from its cache:

conanfile.txt: Installing package
Requirements
    libcurl/7.78.0 from 'conancenter' - Cache
    openssl/1.1.1k from 'conancenter' - Cache
    zlib/1.2.11 from 'conancenter' - Cache
Packages
    libcurl/7.78.0:539b44da7a736f055c2112b92bba7f29d6d3c644 - Cache
    openssl/1.1.1k:6af9cc7cb931c5ad942174fd7838eb655717c709 - Cache
    zlib/1.2.11:6af9cc7cb931c5ad942174fd7838eb655717c709 - Cache

To make this a bit more realistic, we’ll create a small library throwaway/src/mylib.c from a (slightly) modified example from libcurl’s excellent documentation:

#include <stdio.h>
#include <curl/curl.h>

long some_func(char *url)
{
  CURL *curl;
  CURLcode res;
  long code = 0;
  curl_global_init(CURL_GLOBAL_ALL);

  curl = curl_easy_init();
  if(curl) {
    curl_easy_setopt(curl, CURLOPT_URL, url);
    FILE *devnull = fopen("/dev/null", "w+");
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, devnull);

    res = curl_easy_perform(curl);
		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
    if(res != CURLE_OK) {
      fprintf(stderr, "curl_easy_perform() failed: %s\n",
              curl_easy_strerror(res));
    } else {
		  fprintf(stdout, "return code was: %ld\n", code); 
    }
    /* always cleanup */
    curl_easy_cleanup(curl);
    fclose(devnull);
  }
  curl_global_cleanup();
  return code;
}

With the corresponding header file throwaway/src/mylib.h:

/* some useful description goes here */
long some_func(char *url);

Let’s make sure this is all wired up by replacing throwaway/src/main.c with:

#include "mylib.h"

int main(int argc, char *argv[])
{
  some_func(argv[1]);
}

We do need to tell cmake how this fits together, so your CMakeLists.txt should look like:

cmake_minimum_required(VERSION 3.5)
project(throwaway)
enable_testing()

add_definitions("-std=c11")

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

include_directories(src)
add_library(mylib src/mylib.c)
add_executable(main src/main.c)
target_link_libraries(mylib ${CONAN_LIBS})
target_link_libraries(main ${CONAN_LIBS} mylib)

You should then be able to compile and run this:

❯ cmake ..
-- Conan: Adjusting output directories
-- Conan: Using cmake global configuration
-- Conan: Adjusting default RPATHs Conan policies
-- Conan: Adjusting language standard
-- Current conanbuildinfo.cmake directory: /home/axiomiety/repos/throwaway/build
-- Conan: Compiler GCC>=5, checking major version 9
-- Conan: Checking correct version: 9
-- Configuring done
-- Generating done
-- Build files have been written to: /home/axiomiety/repos/throwaway/build
❯ make
[ 50%] Built target mylib
[100%] Built target main
❯ ./bin/main https://httpbin.org
return code was: 200

Adding tests

Now that we have a function that could potentially be tested, let’s write a test for it! Small caveat, that function makes an external call so it’s not quite a unit test - but it’s the intention that counts right?

Let’s create a test a sample test as throwaway/test/sample_test.c:

#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <setjmp.h>
#include <cmocka.h>

#include "mylib.h"

static void test_good_requests(void **state)
{
  assert_int_equal(some_func("https://httpbin.org"), 200);
}

static void test_bad_requests(void **state)
{
  assert_int_equal(some_func("https://httpbin.org/status/404"), 404);
  assert_int_equal(some_func("https://httpbin.org/status/500"), 500);
}

int main(void)
{
  const struct CMUnitTest tests[] = {
    cmocka_unit_test(test_good_requests),
    cmocka_unit_test(test_bad_requests),
  };

  return cmocka_run_group_tests(tests, NULL, NULL);
}

Your CMakeLists.txt should now look like this:

cmake_minimum_required(VERSION 3.5)
project(throwaway)
enable_testing()

add_definitions("-std=c11")

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

include_directories(src)
add_library(mylib src/mylib.c)
add_executable(main src/main.c)
target_link_libraries(mylib ${CONAN_LIBS})
target_link_libraries(main ${CONAN_LIBS} mylib)
add_executable(sample_test test/sample_test.c)
add_test(sample_test sample_test)
target_link_libraries(sample_test mylib cmocka)

At this point, your directory should look more or less like this (with a whole bunch of stuff in throwaway/build if you have been running conan install .. and cmake ..):

❯ tree throwaway
throwaway
├── CMakeLists.txt
├── build
├── conanfile.txt
├── src
│   ├── main.c
│   ├── mylib.c
│   └── mylib.h
└── test
    └── sample_test.c

Running cmake .. and make, we should now have the whole shebang wired up:

❯ cmake ..
-- Conan: Adjusting output directories
-- Conan: Using cmake global configuration
-- Conan: Adjusting default RPATHs Conan policies
-- Conan: Adjusting language standard
-- Current conanbuildinfo.cmake directory: /home/axiomiety/repos/throwaway/build
-- Conan: Compiler GCC>=5, checking major version 9
-- Conan: Checking correct version: 9
-- Configuring done
-- Generating done
-- Build files have been written to: /home/axiomiety/repos/throwaway/build
❯ make
[ 33%] Built target mylib
[ 66%] Built target sample_test
[100%] Built target main
❯ ./bin/sample_test
[==========] Running 2 test(s).
[ RUN      ] test_good_requests
return code was: 200
[       OK ] test_good_requests
[ RUN      ] test_bad_requests
return code was: 404
return code was: 500
[       OK ] test_bad_requests
[==========] 2 test(s) run.
[  PASSED  ] 2 test(s).

And done!

But what about my editor/IDE?

I use VSCode for everything. Heck - I even wrote a plugin for it. It has excellent C support and Microsoft’s own C/C++ Extension should be installed by default (if it isn’t, it’s easy to get!). The only thing I needed to do was to tell it what my include paths was for all those headers. If you try to import a header it doesn’t know about it the little lightbulb will come up asking you to edit your includePath variable. A sample configuration would look like:

${workspaceFolder}/**
~/.conan/data/**