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’s BUILD and WORKSPACE 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 use bazelisk and buildifier.
  • 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. The clangd extension will prompt you to disable ms-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.
  • Hedron’s Compile Commands Extractor for Bazel generates a compile_commands.json file for clangd, 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


  1. 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. ↩︎

  2. Writing this blog post did, too. ↩︎

  3. In particular Hedron recommends Renovate↩︎

  4. 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. ↩︎


Home | Feedback | RSS