Introduction

I have been using Visual Studio Code (vscode) for my C language projects until I’m re-learning this powerful programming language. One of the features that I like about vscode is the ability to build and debug C language projects using a Makefile. In this post, I will show you how to build and debug a C project in vscode using a Makefile.

also, I built a GitHub repository template for this project called c-library-template, where you can use it to create a new C project with the following features:

  • C project structure
  • Makefile
  • Visual Studio Code configuration files
  • GitHub Actions CI/CD workflow

NOTES:

  • This post is based on MacOS, but you can use it on other operating systems like Linux and Windows.
  • I’m using Homebrew to install the required tools and libraries.

Prerequisites

Install prerequisites

Install Visual Studio Code

Download and install Visual Studio Code manually or using the following terminal command:

1
brew install --cask visual-studio-code

Install Visual Studio Code extensions

For each extension mentioned before you can install it using the following steps:

Open Visual Studio Code, click on the Extensions icon on the left sidebar, search for <extension name> and click on the Install button to install the extension.

Also, you can install the extensions using the following terminal commands:

1
2
3
4
code --install-extension ms-vscode.cpptools
code --install-extension ms-vscode.cpptools-extension-pack
code --install-extension ms-vscode.cpptools-themes
code --install-extension ms-vscode.makefile-tools

Install GCC, the GNU Compiler

Install gcc using the following terminal command:

1
brew install gcc

Install GNU Make

Install make using the following terminal command:

1
brew install make

Create a new C library project

I created a new repository template called c-library-template using GitHub CLI:

1
2
3
4
5
6
7
gh repo create slashdevops/c-library-template \
  --add-readme \
  --public \
  --description "C library template" \
  --gitignore "C" \
  --license "Apache-2.0" \
  --clone

Then inside the project directory c-library-template, I created the following project structure:

1
2
3
4
5
mkdir {.vscode,src,include,tests}
touch {.gitignore,README.md,Makefile}
touch {include/linkedlist.h,src/linkedlist.c,tests/main.c}
touch {.vscode/launch.json,.vscode/tasks.json}
touch {.vscode/c_cpp_properties.json,.vscode/settings.json}

While you are in the project directory c-library-template and after running the above commands, I used the tree command, to show the project structure as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
❯ tree -I .git -a

.
├── .gitignore
├── .vscode
│   ├── c_cpp_properties.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── LICENSE
├── Makefile
├── README.md
├── include
│   └── linkedlist.h
├── src
│   └── linkedlist.c
└── tests
    └── main.c

5 directories, 11 files

These are the files and directories that I used as a project structure.

Complementing .gitignore file

I added a extra files and directories to the c-library-template -> .gitignore file executing the following command:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cat << EOF >> .gitignore

# Extra files and directories
lib/
obj/
build/
html/
latex/

.DS_Store
._.DS_Store
**/.DS_Store
**/._.DS_Store
EOF

C code files

The c-library-template -> include/linkedlist.h content is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef LINKEDLIST_H
#define LINKEDLIST_H

#include <stdio.h>

typedef struct Node
{
  // size of the data type
  size_t size;
  void *data;
  struct Node *next;
} Node;

typedef struct List
{
  Node *head;
  size_t size;

  // used to have a reference to the last node, but
  // this is not a circular linked list
  Node *tail;
} List;

List *list_new();
void list_destroy(List *list);
void list_node_destroy(Node *node);
void list_prepend(List *list, Node *node);
void list_append(List *list, Node *node);
void list_prepend_value(List *list, void *value, size_t size);
void list_append_value(List *list, void *value, size_t size);
Node *list_pop(List *list);
size_t list_size(List *list);

#endif // LINKEDLIST_H

The c-library-template -> src/linkedlist.c content is as follows:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include "../include/linkedlist.h"
#include <stdio.h>
#include <stdlib.h>

List *list_new()
{
  List *l = (List *)malloc(sizeof(List));
  l->head = NULL;
  l->tail = NULL;
  l->size = 0;

  return l;
}

void list_destroy(List *list)
{
  if (list->head == NULL)
  {
    free(list);
    return;
  }

  Node *temp_node = NULL;

  while (list->head != NULL)
  {
    temp_node = list->head;
    list->head = temp_node->next;

    list_node_destroy(temp_node);
  }

  free(list);
}

void list_node_destroy(Node *node)
{
  free(node->data);
  free(node);
}

void list_prepend(List *list, Node *node)
{
  if (list == NULL || node == NULL)
  {
    return;
  }

  // store the pointer to the first element prepend to the list
  // to keep track of the tail of the list
  if (list->size == 0)
  {
    list->tail = node;
  }

  node->next = list->head;
  list->head = node;
  list->size++;
}

void list_append(List *list, Node *node)
{
  if (list == NULL || node == NULL)
  {
    return;
  }

  node->next = NULL;

  // store the pointer to the first element prepend to the list
  // to keep track of the tail of the list
  if (list->size == 0)
  {
    list->head = node;
    list->tail = node;
  }

  // add the node to the tail of the list
  list->tail->next = node;
  list->tail = node;

  list->size++;
}

size_t list_size(List *list)
{
  return list->size;
}

void list_prepend_value(List *list, void *value, size_t size)
{
  Node *node = malloc(sizeof(Node));
  node->next = NULL;

  node->data = malloc(size);
  node->data = value;
  node->size = size;

  list_prepend(list, node);
}

void list_append_value(List *list, void *value, size_t size)
{
  Node *node = malloc(sizeof(Node));
  node->next = NULL;

  node->data = malloc(size);
  node->data = value;
  node->size = size;

  list_append(list, node);
}

Node *list_pop(List *list)
{
  if (list == NULL || list->head == NULL)
  {
    return NULL;
  }

  Node *node = list->head;

  if (list->head->next != NULL)
  {
    list->head = list->head->next;
    list->size--;
  }
  else // this is the last
  {
    list->head = NULL;
    list->tail = NULL;
    list->size = 0;
  }

  node->next = NULL;

  return node;
}

The c-library-template -> tests/main.c content is as follows:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#include "../include/linkedlist.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void test_list_new()
{
  List *list = list_new();

  assert(list->size == 0);
  assert(list->head == NULL);
  assert(list->tail == NULL);

  list_destroy(list);
}

void test_list_size_new()
{
  List *list = list_new();

  assert(list_size(list) == 0);

  list_destroy(list);
}

void test_prepend_to_new_list()
{
  List *list = list_new();
  Node *node = malloc(sizeof(Node));

  list_prepend(list, node);

  assert(list_size(list) == 1);

  list_destroy(list);
}

void test_prepend_10()
{
  List *list = list_new();

  for (int i = 0; i < 10; i++)
  {
    Node *node = malloc(sizeof(Node));
    node->next = NULL;
    node->size = sizeof(int);

    node->data = malloc(sizeof(Node));
    memcpy(node->data, &i, sizeof(int));

    list_prepend(list, node);
  }

  assert(list_size(list) == 10);

  Node *node = list->head;
  for (int i = 0; i < 10; i++)
  {
    assert(node->size == sizeof(int));

    int *val = (int *)node->data;
    // printf("node value = %d, i= %d\n", *val, (9 - i));
    assert(*val == (9 - i));

    node = node->next;
  }

  list_destroy(list);
}

void test_append_10()
{
  List *list = list_new();

  for (int i = 0; i < 10; i++)
  {
    Node *node = malloc(sizeof(Node));
    node->next = NULL;
    node->size = sizeof(int);

    node->data = malloc(sizeof(Node));
    memcpy(node->data, &i, sizeof(int));

    list_append(list, node);
  }

  assert(list_size(list) == 10);

  Node *node = list->head;
  for (int i = 0; i < 10; i++)
  {
    assert(node->size == sizeof(int));

    int *val = (int *)node->data;
    // printf("node value = %d, i= %d\n", *val, i);
    assert(*val == i);

    node = node->next;
  }

  list_destroy(list);
}

void test_list_destroy_10()
{
  List *list = list_new();

  for (int i = 0; i < 10; i++)
  {
    Node *node = malloc(sizeof(Node));
    node->data = NULL;
    node->next = NULL;
    list_prepend(list, node);
  }

  assert(list_size(list) == 10);

  list_destroy(list);
}

void test_list_prepend_value()
{
  List *list = list_new();

  for (int i = 10; i > 0; i--)
  {
    int *val = malloc(sizeof(int));
    *val = i;
    list_prepend_value(list, val, sizeof(int));
  }

  assert(list_size(list) == 10);

  // check elemets in the list
  Node *temp_node = list->head;
  for (int i = 0; i < 10; i++)
  {
    int *val = (int *)temp_node->data;
    // printf("value = %d, size = %zu (bytes)\n", *val, temp_node->size);
    assert(*val == i + 1);

    assert(sizeof(int) == temp_node->size);
    temp_node = temp_node->next;
  }

  list_destroy(list);
}

void test_list_append_value()
{
  List *list = list_new();

  for (int i = 10; i > 0; i--)
  {
    int *val = malloc(sizeof(int));
    *val = i;
    list_append_value(list, val, sizeof(int));
  }

  assert(list_size(list) == 10);

  // check elements in the list
  Node *temp_node = list->head;
  for (int i = 0; i < 10; i++)
  {
    int *val = (int *)temp_node->data;
    // printf("value = %d, size = %zu (bytes)\n", *val, temp_node->size);
    assert(*val == 10 - i);

    assert(sizeof(int) == temp_node->size);
    temp_node = temp_node->next;
  }

  list_destroy(list);
}

void test_list_pop_all()
{
  List *list = list_new();

  for (int i = 0; i < 10; i++)
  {
    Node *node = malloc(sizeof(Node));
    node->data = malloc(sizeof(int));
    memcpy(node->data, &i, sizeof(int));
    node->next = NULL;
    node->size = sizeof(int);

    list_prepend(list, node);
  }

  assert(list_size(list) == 10);

  for (int i = 0; i < 10; i++)
  {
    Node *pop_node = list_pop(list);
    assert(pop_node != NULL);
    // int *val = (int *)pop_node->data;
    // printf("value: %d,  size: %zu\n", *val, pop_node->size);

    list_node_destroy(pop_node);
  }

  list_destroy(list);
}

void tests_run_all(void)
{
  test_list_new();
  test_list_size_new();
  test_prepend_to_new_list();
  test_prepend_10();
  test_append_10();
  test_list_destroy_10();
  test_list_prepend_value();
  test_list_append_value();
  test_list_pop_all();
}

int main(void)
{
  tests_run_all();
}

Makefile

This Makefile has a help command that you can use to display the available commands.

You can use the following command to display the help command:

1
make help

The c-library-template -> Makefile content is as follows:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
.DELETE_ON_ERROR: clean

# Where to find tools
TEST_APP = test_linkedlist
TARGET_LIB = linkedlistlib.so

CC_MACOS ?= /opt/homebrew/bin/gcc-13
CC_LINUX ?= /usr/bin/gcc

AR_MACOS ?= /opt/homebrew/bin/gcc-ar-13
AR_LINUX ?= /usr/bin/ar

MEMCHECK_MACOS ?= /usr/bin/leaks
MEMCHECK_LINUX ?= /usr/bin/valgrind

# Determine OS
UNAME_S := $(shell uname -s)

ifeq ($(UNAME_S),Darwin)
	MEMCHECK = $(MEMCHECK_MACOS)
	MEMCHECK_ARGS = --atExit --
	CC = $(CC_MACOS)
	AR = $(AR_MACOS)
endif
ifeq ($(UNAME_S),Linux)
	MEMCHECK = $(MEMCHECK_LINUX)
	MEMCHECK_ARGS =
	CC = $(CC_LINUX)
	AR = $(AR_LINUX)
endif

# Check if OS is supported
ifneq ($(UNAME_S),Darwin)
	ifneq ($(UNAME_S),Linux)
	$(error "Unsupported OS")
	endif
endif

# Check if executables are in PATH
EXECUTABLES = $(CC) $(AR) $(MEMCHECK)
K := $(foreach exec,$(EXECUTABLES),\
  $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH)))

# Compiler and linker flags
CFLAGS = -Wall -Wextra -Werror -Wunused -O2 -g -std=c2x -pedantic # Compiler flags
LDFLAGS = -shared # Linker flags (shared library)  (change to -static for static library)

SRC_DIR := src
OBJ_DIR := obj
LIB_DIR := lib

TEST_SRC_DIR := tests
TEST_OBJ_DIR := obj
BUILD_DIR := build

SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
OBJ_FILES = $(SRC_FILES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

TEST_FILES = $(wildcard $(TEST_SRC_DIR)/*.c)
TEST_OBJS = $(TEST_FILES:$(TEST_SRC_DIR)/%.c=$(TEST_OBJ_DIR)/%.o)

INCLUDE_DIRS = -Iinclude

# Targets
##@ Default target
.PHONY: all
all: clean build ## Clean and build the library

##@ Build commands
.PHONY: clean build
build: $(TARGET_LIB) ## Clean and build the library

$(TARGET_LIB): $(OBJ_FILES) | $(LIB_DIR)
	$(CC) $(LDFLAGS) -o $(LIB_DIR)/$@ $^

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
	$(CC) $(CFLAGS) $(INCLUDE_DIRS) -c -o $@ $<

$(TEST_APP): $(TEST_OBJS) $(LIB_DIR)/$(TARGET_LIB) | $(BUILD_DIR)
	$(CC) $(CFLAGS) $(INCLUDE_DIRS) -o $(BUILD_DIR)/$@ $^

$(TEST_OBJ_DIR)/%.o: $(TEST_SRC_DIR)/%.c | $(TEST_OBJ_DIR)
	$(CC) $(CFLAGS) $(INCLUDE_DIRS) -c -o $@ $<

$(BUILD_DIR):
	@mkdir -p $(BUILD_DIR)

$(OBJ_DIR):
	@mkdir -p $(OBJ_DIR)

$(LIB_DIR):
	@mkdir -p $(LIB_DIR)

##@ Test commands
.PHONY: test
test: clean build $(TEST_APP) ## Run tests
	@echo "Running tests..."
	./$(BUILD_DIR)/$(TEST_APP)

.PHONY: memcheck
memcheck: test ## Run tests and check for memory leaks
	@echo "Running tests with memory check..."
	$(MEMCHECK) $(MEMCHECK_ARGS) ./$(BUILD_DIR)/$(TEST_APP)

##@ Clean commands
.PHONY: clean
clean: ## Clean built artifacts
	@rm -rf $(BUILD_DIR)
	@rm -rf $(OBJ_DIR)
	@rm -rf $(LIB_DIR)

##@ Help commands
.PHONY: help
help: ## Display this help
	@awk 'BEGIN {FS = ":.*##";                                             \
		printf "Usage: make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ \
		{ printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 } /^##@/            \
		{ printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } '                  \
		$(MAKEFILE_LIST)

The Makefile has the following targets:

1
make help

Makefile targets

Visual Studio Code configuration files

The c-library-template -> .vscode/c_cpp_properties.json content is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "configurations": [
    {
      "name": "macos-gcc-arm64",
      "compilerPath": "/opt/homebrew/bin/gcc-13",
      "intelliSenseMode": "macos-gcc-arm64",
      "includePath": [
        "${workspaceFolder}/**",
        "${workspaceFolder}/include/**",
        "/opt/homebrew/lib/gcc/13/**"
      ],
      "defines": [],
      "macFrameworkPath": [
        "${workspaceFolder}/**",
        "/System/Library/Frameworks"
      ],
      "cStandard": "c23",
      "cppStandard": "c++23",
      "configurationProvider": "ms-vscode.makefile-tools",
      "browse": {
        "path": [
          "${workspaceFolder}/**",
          "${workspaceFolder}/include/**",
          "/opt/homebrew/lib/gcc/13/**"
        ]
      }
    }
  ],
  "version": 4
}

The c-library-template -> .vscode/launch.json content is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "version": "2.0.0",
  "configurations": [
    {
      "name": "C Debug -> linkedlist Makefile",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/build/test_linkedlist",
      "args": [],
      "stopAtEntry": true,
      "cwd": "${workspaceFolder}",
      "environment": [],
      "externalConsole": false,
      "MIMode": "lldb",
      "preLaunchTask": "make"
    }
  ]
}

The c-library-template -> .vscode/tasks.json content is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "cppbuild",
      "label": "make",
      "command": "make && make test",
      "args": [],
      "options": {
        "cwd": "${workspaceFolder}"
      },
      "problemMatcher": [
        "$gcc"
      ],
      "group": "build",
      "detail": "Build our program using make"
    }
  ],
}

The c-library-template -> .vscode/settings.json content is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "C_Cpp.errorSquiggles": "enabled",
  "C_Cpp.enhancedColorization": "enabled",
  "C_Cpp.intelliSenseEngine": "Tag Parser",
  "files.associations": {
    "*.template": "yaml",
    "cstdlib": "c",
    "__hash_table": "c",
    "__split_buffer": "c",
    "array": "c",
    "bitset": "c",
    "deque": "c",
    "initializer_list": "c",
    "queue": "c",
    "span": "c",
    "stack": "c",
    "string": "c",
    "string_view": "c",
    "unordered_map": "c",
    "vector": "c",
    "format": "c"
  },
}

Build and Debug the C project

  1. Open the project directory c-library-template in Visual Studio Code:
1
2
cd c-library-template/
code .
  1. Build the project using the make command:
1
make
  1. Run the tests using the make test command:
1
2
3
4
5
# Run the tests
make test

# Run the tests and check for memory leaks
make memcheck
  1. Debug the project using the make task:
  • Set your breakpoints in the source code.
  • Press Cmd + Shift + P to open the command palette.
  • Type C/C++: Debug and select C/C++: Debug C/C++ File.
  • Select the C Debug -> linkedlist Makefile task to build and start debugging the project.

Github Actions CI/CD Workflow

I created a GitHub Actions CI/CD workflow to build and test the project on every push to the main branch.

The c-library-template -> .github/workflows/ci.yml content is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
name: C CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Install dependencies
      run: |
        sudo apt install software-properties-common -y
        sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
        sudo apt-get update -y
        sudo apt-get install -y gcc-13 valgrind        

    - name: Set up gcc-13
      run: |
        sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 90        

    - name: Check versions
      run: |
        gcc --version
        make --version        

    - name: make build
      run: make build

    - name: make test
      run: make test

    - name: make memcheck
      run: make memcheck

  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
    - uses: actions/checkout@v4

    - name: Install dependencies
      run: |
        sudo apt install software-properties-common -y
        sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
        sudo apt-get update -y
        sudo apt-get install -y gcc-13 valgrind        

    - name: Set up gcc-13
      run: |
        sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 90        

    - name: Check versions
      run: |
        gcc --version
        make --version        

    - name: make build
      run: make build

Conclusion

When you are (re-)learning a new programming language, it is important to have a good project structure and tools to help you build and debug your projects. I always spend time finding the way to make my life easier.

I hope this post helps you to build and debug your C projects using Visual Studio Code with a Makefile.

Leave a comment if you have any questions or suggestions.