Have you ever heard people say C is dead?

For many people today, it may seem so. In spite of this, C still runs the computer world. And C could benefit you even when your work lays far away from manual memory management.

For example, imagine a web app in Erlang and mobile app in Java being using the same code to calculate taxes without even being connected or using a separate service.

I recently ran into this situation by myself when faced a need to parse a special DSL. I found out that my best option is to write the core library in C and then use it with FFI in consumers' code.

The single best way to manage C projects today is to use CMake. The only caveat here is like, its documentation is excessive, but lacks some simple examples. Here I'm writing about my experiences setting up a CMake project for shared library.

Getting ready

Some preliminary requirements is to install Clang and CMake. Simplest way to do it on Mac or Linux systems is to use Homebrew:

brew install clang cmake

Some configuration then has to be made so you can fully utilize Clang. Add following lines to your .bashrc file:

# Configure LLVM: export CC=clang export CXX=clang++ export PATH="$(brew --prefix)/opt/llvm/bin:$PATH" export LDFLAGS="-L$(brew --prefix)/opt/llvm/lib -Wl,-rpath,$(brew --prefix)/opt/llvm/lib"

Create a project and structure it like this:

awesome/ ├── CMakeLists.txt ├── include/ │   └── calculator/ │   └── calculator.h ├── src/ │   └── calculator.c └── tests/ ├── increment.c └── decrement.c

CMake configuration is actuall pretty self-explanatory, still:

cmake_minimum_required(VERSION 3.20.1) project(calculator VERSION 1.0.0 DESCRIPTION "Cool stuff." HOMEPAGE_URL "https://github.com/your-name/calculator" LANGUAGES C) set(CMAKE_C_FLAGS "-Weverything") add_library(calculator SHARED) target_include_directories(calculator PUBLIC "include") target_include_directories(calculator PRIVATE "include") target_sources(calculator PUBLIC "src/calculator.c") enable_testing() macro(push_test name) add_executable(test_${name} tests/${name}.c) target_link_libraries(test_${name} calculator) add_test(${name} test_${name}) endmacro() push_test(increment) push_test(decrement)

How to implement the test

CMake uses a framework called CTest to provide testing capabilities. Any test is just a C source file containing main function. To determine if test is passed or failed CTest checks the return value of the test executable. For example, calculator.c contains two functions with signatures like this:

int increment(int); int decrement(int);

Single test can roughly be written like this:

#include <stdlib.h> #include <string.h> #include "calculator/calculator.h" int main(void) { int target = 0; if (decrement(increment(target)) == target) { return EXIT_SUCCESS; } return EXIT_FAILURE; }

As you can see, CTest uses exit code of the test program to see if verification is failed or passed. It is possible to set it up to parse for specific output instead of relying on return value.

Compiling the code

Create a separate directory for your artifacts, navigate to it, then call CMake pointing to the root of your project. The Makefile will be generated.

mkdir stage cd stage cmake .. make

When done, you'll see all the expected artifacts within the stage directory: libcalculator.so file and test executables.

Running tests

Special test target will be generated by CMake, so when you hit make test within the build folder, CMake will execute known tests and report a results. In case your tests produce some output, CTest hides it unless you use "--extra-verbose" option on CTest executable directly, please note using just "--verbose" is not enough:

cmake --extra-verbose ..

It is boring to do manually, so you can add custom target with any options you want already applied.

P. S. If you haven't been working with C for a while I would recommend «Modern C» book by Jens Gustedt for a refresher. It is available here.