Setup Guide for C/C++ Programming on VSCode

10 min read Original article ↗

Usman Mehmood

This is a guide for creating, building and running a C/C++ project with multiple files, libraries and configurations, in VSCode.

Press enter or click to view image in full size

Background

During my university days and even after graduation, the process of creating a C or C++ application from the scratch was unclear to me.

Until about 2 years into my career, the only way I knew how to create C and C++ applications was to fire up VSCode, make a new main.c or .cpp and hope that VSCode is able to generate the missing files required to compile, run and debug it. Sometimes it did.

But most of the times it would show some error that some file or configuration is missing, after which I would have to resort to Eclipse and its behind the scenes magic for creating, building and running my applications.

For students such as myself, that are studying something like mechatronics engineering and not computer science, we aren’t taught what happens under the “build” or “compile” buttons in our IDEs. And so I have put up this guide, for students and beginners that want to know how to compile their C and C++ codes.

Build Process

Compiling, Running and Debugging

To compile or build your program, you need a C compiler. Which can be GCC. And to debug it, you’d need a debugger. Which can be GDB. There are other compilers and debuggers but I find it easier to use just these two.

Check for GCC and GDB

Go to the terminal and type gcc --version into it. If you get something like this:

gcc.exe (GCC) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

It means GCC is installed. Now put gdb --version into the terminal and you should get something like this:

GNU gdb (GDB) 14.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

It means that GDB is also installed. And that VSCode should be able to access both.

If that’s not the case, just search for “VSCode C C++” or something similar, and it will lead you to this page that shows you the exact setup under the heading “Example: Install MinGW-x64 on Windows”.

Or an alternative and much easier way to install all the tools mentioned in this guide is via package managers. This process is explained towards the end of the article.

Compiling with GCC

If you have just one main.c, then you can just pass it straight to GCC and it
will compile, no questions asked.

gcc ./main.c -o program.exe

This is what its output would look like, the .exe is compiled.

Press enter or click to view image in full size

Result of single-file compilation.

If you want to debug it then also tell GCC to add a debug symbols using -g or -g3 and if you want to set the optimization level then pass one of these -O0, -O1, or -O3.

If you have a additional files in a folder, with the following structure:

project
|
|- library_1
| |
| |- library_1.h
| |- library_1.c
|
|- main.c

you can pass something like this to GCC.

gcc main.c -I ./library_1 library_1/library_1.c -o program.exe

And it would compile.

Press enter or click to view image in full size

Result of multi-file compilation

But as the number of files and increases, this command will become insanely large. For example, say you have a project with 4 libraries, for performing the 4 basic math functions.

project
|
|- adder
| |
| |- adder.h
| |- adder.c
|
|- subtractor
| |
| |- subtractor.h
| |- subtractor.c
|
|- multiplier
| |
| |- multiplier.h
| |- multiplier.c
|
|- divider
| |
| |- divider.h
| |- divider.c
|
|- main.c

To compile this, the GCC command would be:

gcc main.c ./adder/adder.c ./subtractor/subtractor.c ./multiplier/multiplier.c ./divider/divider.c -I ./adder -I ./subtractor -I ./multiplier -I ./divider -o program -O0 -g3

Let’s make it a little easier to read. Do note that the \ character is used to split the larger single-line command into a smaller one.

gcc main.c                \
./adder/adder.c \
./subtractor/subtractor.c \
./multiplier/multiplier.c \
./divider/divider.c \
-I ./adder \
-I ./subtractor \
-I ./multiplier \
-I ./divider \
-o program -O0 -g3

With the number of files growing, the compilation command keeps getting bigger and bigger into something quite unmanageable.

Using CMake

When there are lots of files with many folders, a build system is used to tell the compiler where the files are and which commands to use for compiling them. CMake is one such system. It manages the builds and passes on stuff to GCC automatically. A simple CMakeLists.txt would be something like this.

# Set the name of the project to the folder name
cmake_minimum_required(VERSION 3.19)
get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
project(${PROJECT_NAME} VERSION 0.1 LANGUAGES C CXX)

# Generate files for IntelliSense
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Add your executable and attach it to main.c
add_executable(${PROJECT_NAME} main.c)

# Add your library folders
add_subdirectory(library_1)
add_subdirectory(library_2)
add_subdirectory(library_3)
add_subdirectory(library_4)

But keep in mind that each library folder would also have a CMakeLists.txt. Personally I like to make a folder with the library name, make a .c and .h file with the same name. Like this.

library_1
|- library_1.h
|- library_1.c
|- CMakeLists.txt

And then put this in the CMakeLists.txt of library_1.

# Get the library name automatically
get_filename_component(CURRENT_DIR_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)

# Add the .c file
target_sources(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/${CURRENT_DIR_NAME}.c)

# Include the .h file
target_include_directories(${PROJECT_NAME} PRIVATE .)

It automatically picks up the library name and the .c and .h file names, so you can just copy paste this in all your libraries. And then you can build like this:

cmake -G Ninja -B build
ninja -C build

Do note that a second software called Ninja is being used as well. Once the build is complete, it’ll put the executable in the build folder.

project
|
|- build
| |
| |- lots_of_other_files
| |- your_project.exe <--- this is your program
|
|- library_1
| |
| |- library_1.h
| |- library_1.c
|
|- main.c

Press enter or click to view image in full size

Result of CMake and Ninja compilation.

If you want to build and execute in a single line, you can combine the commands using the && token, like this:

cmake -G Ninja -B build && ninja -C build && .\build\your_project.exe

Integrating VSCode Features

  • IntelliSense
  • Tasks
  • Debugging

IntelliSense

VSCode has features like code navigation, viewing function documentation, auto-complete etc, which combined together are referred to as “IntelliSense”. IntelliSense is enabled by creating a file called c_cpp_properties.json in a folder called .vscode which is placed inside your project folder. The c_cpp_properties.json file tells VSCode which folders to look into for code files, which version of the language to use.

Here’s an example of a typical c_cpp_properties.json file.

{
"configurations":
[
{
"name" : "GCC",
"includePath" : [ "${workspaceFolder}/**" ],
"compilerPath" : "C:/msys64/mingw64/bin/gcc.exe",
"cStandard" : "c11",
"cppStandard" : "c++11",
"intelliSenseMode": "windows-gcc-x64",
"compileCommands" : "build/compile_commands.json"
}
],
"version": 4
}
  • name is just a name of this configuration, it can be anything.
  • includePath suggests which paths, i.e., which files and folders VSCode
    should look for. These are the source files that you want VSCode to be
    aware of. It is set to [ "${workspaceFolder}/**" ] to tell VSCode to
    look into the entire project folder.
  • compilerPath tells VSCode where the intended compiler is located.
  • cStandard and cppStandard indicate which language versions are intended to be used.
  • intelliSenseMode further specifies the intended compiler mode and target.
  • compileCommands points to a special file generated by CMake, which contains all the commands passed to GCC. This further improves IntelliSense by providing the exact file paths and macros used.

Keep in mind these settings have nothing to do with the actual compilation. They simply tell VSCode which compiler is used, what mode it is used in, which files are included in the code and what the target architecture it. IntelliSense then uses this information to give accurate auto-complete, check for compile-time errors, and view documentation.

Tasks

‘Tasks’ are terminal commands that can be invoked by VSCode itself, and can be used to do things like invoking build commands, cleaning directories and making files. Basically anything that can be done via the terminal. For creating tasks, a file named tasks.json has to be created in the .vscode folder.

Here’s a typical tasks.json configuration.

{
"version": "2.0.0",
"tasks" :
[
{
"label" : "cmake_generate",
"type" : "shell",
"command": "cmake -GNinja -Bbuild"
},
{
"label" : "ninja_build",
"type" : "shell",
"command": "ninja -C build"
},
{
"label" : "run_executable",
"type" : "shell",
"command": "./build/${workspaceFolderBasename}.exe"
},
{
"label" : "build_and_run",
"type" : "shell",
"dependsOrder": "sequence",
"dependsOn" :
[
"cmake_generate",
"ninja_build",
"run_executable"
],
"command": "echo \"All tasks executed successfully.\""
}
]
}

Each task has a simple structure. The label indicates the name of the task,
type indicates that it is a shell command, and command states the actual shell command. The last task combines the previous tasks by using the a special
dependsOn property, indicating all the tasks that need to be completed before executing its own command. These tasks are responsible for invoking the builds. dependsOrder is set to sequence, indicating that the tasks mentioned in dependsOn need to be executed in sequence and not in parallel.

Debugging

The debug configurations are placed in the launch.json file.

{
"configurations": [
{
"name" : "Debug Config",
"preLaunchTask" : "build_and_run",
"type" : "cppdbg",
"request" : "launch",
"program" : "${workspaceRoot}/build/${workspaceFolderBasename}.exe",
"args" : [],
"stopAtEntry" : true,
"cwd" : "${workspaceRoot}",
"environment" : [],
"externalConsole": false,
"MIMode" : "gdb",
"miDebuggerPath" : "c:/mingw/bin/gdb.exe",
}
]
}

The first important bit in this is the program parameter which should point to the program created after building. You can see that the cmake_generate task uses the command cmake -G Ninja -B build which indicates that the executable would be in a folder named build, and the root CMake names the project the same as its folder name using . And so the program property of launch.json is also pointing to the same executable.

The second important bit is the preLaunchTask parameter which tells the debugger to perform a task before it starts debugging. It’s been set to the build_and_run task, so every time you debug, it will rebuild to let you use the latest code.

And the third important but is the miDebuggerPath variable. This must point to the debugger executable you want to use.

Code Files Available

All the CMake related code files used in this article are provided in this GitHub repo.

https://github.com/usmanmehmood55/cmake_sample

Installing Tools Via Package Manager

Often times installing different build tools requires a lot of Google searches, troubleshooting and mental exhaustion. That being said, it is a very good exercise, and I highly recommend trying to install different tools on your own. It helps develop an understanding of how build tools are “installed” and how they are accessed by other programs.

Still, when trying to hit the ground running, and getting to the actual coding part as soon as possible, package managers can come in handy. A package manager, as the name suggests, takes care of different tools and packages, including installing, updating and removing them. And while Linux and MacOS have their build-in package managers, for Windows an external one has to be installed. In this case, a package manager called “Scoop” can be used.

Windows

To install Scoop itself, open a PowerShell and use these commands. They have been copied from the tool’s website.

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

Once Scoop installed, use the install command to install the build tools.

scoop install gcc gdb cmake ninja

Linux

The default package manager, “APT” can be used. Do note that Ninja would be listed as ninja-build.

apt-get install gcc gdb cmake ninja-build

MacOS

The default package manager, “Homebrew” can be used.

brew install gcc gdb cmake ninja

VSCode Extension

I have made a VSCode extension that does all of these steps for you, and creates preconfigured CMake projects. It also provides buttons for cleaning, building, running, debugging and testing those projects so you don’t have to deal with the terminal again and again. And it also checks for missing build tools and installs them so the you don’t have to deal with installations and configurations.

It’s called C C++ Toolkit.

Press enter or click to view image in full size

My extension, C C++ Toolkit