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:
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:
With the corresponding header file throwaway/src/mylib.h
:
Let’s make sure this is all wired up by replacing throwaway/src/main.c
with:
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
:
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/**