VSCode C++ setup
Posted: 2023-09-23
I just set up VSCode for a tiny C++ project. There are a lot of extensions out there to manage various parts of the config. I set up reproducible builds and accurate IntelliSense integration in a way that feels lightweight compared to the other options I saw.
Goals
While on paternity leave, I wanted something interesting to play around with for less than an hour at a time. There’s some downtime, and when I’m not sleeping there are only so many crosswords and sudokus to play.1 I may write up the actual project at some point, but I was conveniently nerdsniped along the way with general VSCode setup for C++ projects! That turned out to fit the bill nicely.2
For trivial projects, a system-wide toolchain (apt install gcc
) is totally
fine. I wanted to do some performance benchmarking, though. For that, it’s
convenient to have fully reproducible builds and tests. That way I can
confidently develop on my local machine
(WSL) and test on a few
different cloud VMs, and even different architectures. I also want to get all
the VSCode IntelliSense integrations: jumping to headers, semantic autocomplete,
highlighting, etc.
In hopes of helping someone else, here’s my setup:
Applications
- VSCode provides a pretty pleasant development environment, certainly the best I’ve used on Windows.
bazelisk
is a convenient way to use a specific bazel version for a workspace and to handle bazel upgrades.buildifier
formats bazel’sBUILD
andWORKSPACE
files.
VSCode extensions
Between bazel and C++, there are a lot of extension options in VSCode, including Microsoft’s recommended C/C++ integration. I found that three extensions were sufficient.
bazel.build
provides bazel file highlighting & IntelliSense integration. It can be configured to usebazelisk
andbuildifier
.clangd
provides C++ highlighting & IntelliSense, and most usefully for me it can use project-specific compilation commands for accurate headers and dependencies.ms-vscode.cpptools
is Microsoft’s C/C++ integration. Theclangd
extension will prompt you to disablems-vscode.cpptools
’ IntelliSense. I use this for debugger integration.
I specifically am not using the Microsoft C/C++ extension. We’ll see if I come to regret that!
Bazel workspace
- Grail’s LLVM toolchain for
Bazel provides a hermetic
toolchain, fixing the LLVM version, C++ standard version, and more.
- My version of bazel also benefits from a
.bazelrc
file. Otherwise, I’d have to pass a commandline flag to every build to use this toolchain. - This was migrated to the Bazel Central Registry recently, which reduced the boilerplate required to set it up.
- My version of bazel also benefits from a
- Hedron’s Compile Commands Extractor for
Bazel
generates a
compile_commands.json
file forclangd
, notably in a way that works with both the hermetic LLVM toolchain and the default local toolchain. Other extensions I investigated would only work with the local toolchain. .bazeliskrc
in the workspace root fixes the bazel version, which makes it easy to upgrade bazel and remove the newly-unnecessary.bazelrc
in one commit.
Config
I wind up with a VSCode directory that looks like:
project
├─.bazeliskrc
├─.bazelrc
├─BUILD
├─MODULE.bazel
└─WORKSPACE
.bazeliskrc
1USE_BAZEL_VERSION=6.x
.bazelrc
1# It may be possible to remove this in a future version of bazel 2build --incompatible_enable_cc_toolchain_resolution
BUILD
1load("@hedron_compile_commands//:refresh_compile_commands.bzl", "refresh_compile_commands") 2 3refresh_compile_commands( 4 name = "refresh_compile_commands", 5 targets = { 6 "//...:all": "", 7 }, 8)
MODULE.bazel
1module(name = "name", version = "0.0") 2 3bazel_dep(name = "toolchains_llvm", version = "0.10.3") 4 5# Configure the LLVM toolchain. 6llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm") 7llvm.toolchain( 8 llvm_version = "15.0.6", 9 cxx_standard = {"": "c++20"}, 10) 11 12use_repo(llvm, "llvm_toolchain") 13 14register_toolchains("@llvm_toolchain//:all")
WORKSPACE
1# Hedron's Compile Commands Extractor for Bazel 2# https://github.com/hedronvision/bazel-compile-commands-extractor 3load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 5http_archive( 6 name = "hedron_compile_commands", 7 sha256 = "ed5aea1dc87856aa2029cb6940a51511557c5cac3dbbcb05a4abd989862c36b4", 8 strip_prefix = "bazel-compile-commands-extractor-e16062717d9b098c3c2ac95717d2b3e661c50608", 9 url = "https://github.com/hedronvision/bazel-compile-commands-extractor/archive/e16062717d9b098c3c2ac95717d2b3e661c50608.tar.gz", 10) 11 12load("@hedron_compile_commands//:workspace_setup.bzl", "hedron_compile_commands_setup") 13 14hedron_compile_commands_setup()
Caveats
It’s a stretch to call this “lightweight” between the extension setup and ~40
lines of boilerplate, but it at least hits both of my goals: accurate
IntelliSense with a hermetic toolchain and build system. It’s a little annoying
that I have to bazel run :refresh_compile_commands
to regenerate
compile_commands.json
; there is probably a way to make VSCode run it for me
automatically on every BUILD
/WORKSPACE
file change.
Technically, buildifier
is not required, but it makes the BUILD
/WORKSPACE
autoformatting much better.
I have to keep both the external library references (the compile commands extractor and the LLVM toolchain) up to date. For my projects it’s no burden to do that manually, but it might be possible to automate those things in bigger projects.3
The LLVM toolchain downloads the compiler on the first build. This one-time cost is fine by me, but does obviously slow down the very beginning of the project.
It’s also possible that more complex projects would require a more complex
:refresh_compile_commands
target. That would be a pretty annoying burden.
I worked at Google for a long time, so perhaps unsurprisingly I grok
bazel/blaze’s model for hermetic builds and dependency tracking, including a
fixed toolchain. CMake, Ant/Maven, and make/autotools are also fine build
systems with a history of results, although I’ve never used any of them with a
fixed toolchain4 and I’ve only written Makefile and BUILD files from
scratch. As long as there’s a way to generate the compile_commands.json
file
that clangd
expects, it should be possible to get the same IntelliSense
behavior. The clangd
extension’s docs include a CMake example.
Finally, if someone else depends on my project via an external bazel reference, then bazel will use their workspace’s toolchain, not the one I specified. That’s working as intended for bazel: the binary outputs should be in control of the toolchain, and everything should be built together and linked statically by default.
Thoughts?
This setup fits my needs, but it might be possible to do it with less boilerplate. If you have a neat way to set up C++ projects, let me know via the Feedback link below!
--Chris
I didn’t want anything too interesting, though: when it’s time to play or take care of the baby I can stop thinking about the project very easily. ↩︎
Writing this blog post did, too. ↩︎
The closest I’ve seen was a clever former colleague who always ran
make
in a docker container to fix the compilation environment. This was slow enough that he found a way to extract the make compilation database into a set of blaze rules. ↩︎