It has been quite a long time since I wrote a thing in C++ language. Such a long break allowed me to explore different languages and technologies and see what "the other" world looks like. I have to admit that I am really thrilled with how fast one can have a working application written in JAVA. I also admire building systems and how everything can be automated. Maybe Ant or Maven is not my first choice, but Gradle is pretty neat. It is not light-speed fast, but the single command can get needed dependencies, compile everything and produce something executable. Need another dependency? You just add a few lines in Gradle files. No need to separately download, compile and install them in the operating system. And that's pretty charming. I believe that tools such as Make or CMake are pretty nice but are quite mature as a lot of time has passed since their initial release. And IDEs? Hmmm... Eclipse? It has nice features but so so slow (it's JAVA, right? 😂). QtCreator? Fewer features than Eclipse, but pretty stable though. CLion? Well, it's a nice but paid application. So maybe the old school way - grep, sed, vim, and ctags? Such a pain in the ... neck. But actually, I know people who still do that. 🤣 So I started to wonder how things have changed. I decided to experiment with the build system used by Google called Bazel.
Build with Bazel
As per the authors' claim, Bazel gives the ability to build and test software of any size, quickly and reliably. Big tech companies like Google, Str,ipe and Dropbox use Bazel to build heavy-duty, mission-critical infrastructure, services, and applications. For more information please refer to Bazel's website.
Bazel
Bazel can be installed on macOS using homebrew as follows.
brew install bazel
Once installation completes, we should be able to issue bazel
command in the terminal.
❯ bazel
WARNING: Invoking Bazel in batch mode since it is not invoked from within a workspace (below a directory having a WORKSPACE file).
[bazel release 5.3.2-homebrew]
Usage: bazel <command> <options> ...
Available commands:
analyze-profile Analyzes build profile data.
aquery Analyzes the given targets and queries the action graph.
build Builds the specified targets.
canonicalize-flags Canonicalizes a list of bazel options.
clean Removes output files and optionally stops the server.
coverage Generates code coverage report for specified test targets.
cquery Loads, analyzes, and queries the specified targets w/ configurations.
dump Dumps the internal state of the bazel server process.
fetch Fetches external repositories that are prerequisites to the targets.
help Prints help for commands, or the index.
info Displays runtime info about the bazel server.
license Prints the license of this software.
mobile-install Installs targets to mobile devices.
print_action Prints the command line args for compiling a file.
query Executes a dependency graph query.
run Runs the specified target.
shutdown Stops the bazel server.
sync Syncs all repositories specified in the workspace file
test Builds and runs the specified test targets.
version Prints version information for bazel.
Getting more help:
bazel help <command>
Prints help and options for <command>.
bazel help startup_options
Options for the JVM hosting bazel.
bazel help target-syntax
Explains the syntax for specifying targets.
bazel help info-keys
Displays a list of keys used by the info command.
Now we need to create a workspace for our application and at the same time, I strongly advise you to version it, however, I will skip that part. So let's create a new directory and create WORKSPACE.bazel
file in it.
❯ mkdir bazel-sandbox
❯ cd bazel-sandbox
❯ touch WORKSPACE.bazel
💡 Please note that a .bazel extension is not required - it can be omitted.
Issuing the build command will start the local server and run the build.
❯ bazel build
Starting local Bazel server and connecting to it...
WARNING: Usage: bazel build <options> <targets>.
Invoke `bazel help build` for full description of usage and options.
Your request is correct, but requested an empty set of targets. Nothing will be built.
INFO: Analyzed 0 targets (0 packages loaded, 0 targets configured).
INFO: Found 0 targets...
INFO: Elapsed time: 2.172s, Critical Path: 0.02s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
So good so far, Bazel is working, but it has nothing to build. Now we can add some C++ code and configure targets.
Hello world
Let's start with creating a directory structure and a simple hello world application.
❯ mkdir main
❯ cd main
❯ touch main.cc
❯ touch BUILD.bazel
Now we can print "Hello, world!" in main.cc
.
To be able to run the build in Bazel, we have to create now BUILD.bazel
file with a defined executable target.
Having those artifacts we are ready to go with the build.
❯ bazel build main:main
INFO: Analyzed target //main:main (36 packages loaded, 166 targets configured).
INFO: Found 1 target...
INFO: From Linking main/main:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
Target //main:main up-to-date:
bazel-bin/main/main
INFO: Elapsed time: 19.891s, Critical Path: 1.23s
INFO: 8 processes: 6 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 8 total actions
Build has finished and produced executable binary! We can also run it using Bazel.
❯ bazel run main:main
INFO: Analyzed target //main:main (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //main:main up-to-date:
bazel-bin/main/main
INFO: Elapsed time: 0.156s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
hello, world!
hello, world indeed! :)
That has been pretty easy and straightforward. However, in real-life cases, a project won't be as simple as this example and we will probably have multiple libraries that compile and link to the executable. So let's try with multiple targets.
Multi-target project
First, let's create a lib directory in the project's root and add the necessary files there.
❯ mkdir lib
❯ cd lib
❯ touch BUILD.bazel
❯ touch word_generator.h
❯ touch word_generator.cc
We will create a word generator that will provide hello, world! :) Let's start with the header file.
And implementation of course.
We have created helloworld_generator that implements word_generator and provides a "hello, world!" string. The last bit is to provide the Bazel build file.
First of all, we have to load the cc_library module and then use it to create lib. We provide a name for it, paths for headers and source files as well as define its visibility (i.e. it will be visible to the main module only) and provide additional compilation parameters. In our case, we enable C++20 features.
Now we will slightly modify our main module to make use of our brand-new generator.
Now we use the generator to provide the string for output. Next, we have to update the module's dependencies in BUILD.bazel file.
We added deps section to provide explicitly what's needed to create an executable binary. Additionally, we build the main module using the C++20 standard. Now we can rerun the build.
❯ bazel build main:main
INFO: Analyzed target //main:main (37 packages loaded, 169 targets configured).
INFO: Found 1 target...
INFO: From Linking main/main:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
Target //main:main up-to-date:
bazel-bin/main/main
INFO: Elapsed time: 17.007s, Critical Path: 10.52s
INFO: 12 processes: 8 internal, 4 darwin-sandbox.
INFO: Build completed successfully, 12 total actions
And then execute the binary.
❯ bazel run main:main
INFO: Analyzed target //main:main (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //main:main up-to-date:
bazel-bin/main/main
INFO: Elapsed time: 0.137s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
hello, world!
So good so far. We have a main target, a lib, now let's add a target for testing.
Integrating Google Test
Let's start with creating a dedicated directory for tests.
❯ mkdir test
❯ cd test
❯ touch word_generator_test.cc
❯ touch BUILD.bazel
We have to write a simple test that will verify whether provided string is correct.
To be able to run tests, we have to create now BUILD.bazel
file with a defined executable test target.
We name the target test, provide word_generator_test.cc as the source and refer to the external dependency which is Google Test. Now we have to get the dependency. Therefore we will update WORKSPACE.bazel
file.
Firstly we load http_archive to be able to fetch archives from GitHub for instance. Then we define one as gtest. strip_prefix is very useful as we don't pollute code with release name, but rather use gtest/gtest.h
in code.
The last step is to update the lib module. We have to make it visible to the test module.
Having all those steps done, we are ready to execute the first test.
❯ bazel run test:test
INFO: Analyzed target //test:test (51 packages loaded, 667 targets configured).
INFO: Found 1 target...
INFO: From Linking lib/liblib.so:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
INFO: From Linking external/gtest/libgtest_main.so:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
INFO: From Linking external/gtest/libgtest.so:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
INFO: From Linking test/test:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
Target //test:test up-to-date:
bazel-bin/test/test
INFO: Elapsed time: 6.707s, Critical Path: 6.02s
INFO: 35 processes: 15 internal, 20 darwin-sandbox.
INFO: Build completed successfully, 35 total actions
INFO: Build completed successfully, 35 total actions
exec ${PAGER:-/usr/bin/less} "$0" || exit 1
Executing tests from //test:test
-----------------------------------------------------------------------------
Running main() from gmock_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from hello_world_test
[ RUN ] hello_world_test.hello
test/word_generator_test.cc:7: Failure
Expected equality of these values:
generator->next()
Which is: "hello, world!"
"Hello, world!"
[ FAILED ] hello_world_test.hello (0 ms)
[----------] 1 test from hello_world_test (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] hello_world_test.hello
1 FAILED TEST
external/bazel_tools/tools/test/test-setup.sh: line 351: 16637 Killed: 9 sleep 10
And the test fails! 😂
Integrating Google Benchmark
C++ is mainly used to write highly efficient, high-performance applications. So during development, it happens that we want to compare different implementations and choose a faster one. To address that need really neat library has been created, called Google Benchmark.
Let's modify word_generator a bit and introduce another method that will provide the ability to create a string with multiple hello, world!s.
Now the most straightforward way to implement the method is to call the next() method a couple of times.
Again, let's create a dedicated directory for a benchmark test.
❯ mkdir benchmark
❯ cd benchmark
❯ touch benchmark_test.cc
❯ touch BUILD.bazel
We have to write a simple benchmark - we measure the cost of generating multiple strings. We will start by passing 1 into the method and ending with 100. The range multiplier is set to 2, so measurements will be taken for the following numbers: 1, 2, 4, 8, 16, 32, 64, and 100.
To be able to run the benchmark, we have to create now BUILD.bazel
file with a defined executable test target.
Defining this test target is similar to Google Test one. The major difference here is we have to provide Google Benchmark as a dependency.
To get the Google benchmark dependency we will update WORKSPACE.bazel
file again.
And the last step is to update the lib module and make it visible to the benchmark module.
Now we are ready to execute the benchmark.
❯ bazel run benchmark:benchmark
INFO: Analyzed target //benchmark:benchmark (49 packages loaded, 655 targets configured).
INFO: Found 1 target...
INFO: From Linking lib/liblib.so:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
INFO: From Linking external/benchmark/libbenchmark_main.so:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
INFO: From Linking benchmark/benchmark:
ld: warning: -undefined dynamic_lookup may not work with chained fixups
Target //benchmark:benchmark up-to-date:
bazel-bin/benchmark/benchmark
INFO: Elapsed time: 51.476s, Critical Path: 5.60s
INFO: 45 processes: 18 internal, 27 darwin-sandbox.
INFO: Build completed successfully, 45 total actions
INFO: Build completed successfully, 45 total actions
exec ${PAGER:-/usr/bin/less} "$0" || exit 1
Executing tests from //benchmark:benchmark
-----------------------------------------------------------------------------
2023-04-02T18:34:02+00:00
Running /private/var/tmp/_bazel_slysj/87594810adcd72e15cf8187345326b1b/execroot/__main__/bazel-out/darwin-fastbuild/bin/benchmark/benchmark.runfiles/__main__/benchmark/benchmark
Run on (16 X 2300 MHz CPU s)
CPU Caches:
L1 Data 32 KiB
L1 Instruction 32 KiB
L2 Unified 256 KiB (x8)
L3 Unified 16384 KiB
Load Average: 2.53, 2.06, 1.88
***WARNING*** Library was built as DEBUG. Timings may be affected.
-----------------------------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------------------------
benchmark_helloworld_generator/1 142 ns 142 ns 4823362
benchmark_helloworld_generator/2 293 ns 293 ns 2429181
benchmark_helloworld_generator/4 526 ns 526 ns 1339636
benchmark_helloworld_generator/8 985 ns 984 ns 699559
benchmark_helloworld_generator/16 1870 ns 1869 ns 371402
benchmark_helloworld_generator/32 3448 ns 3448 ns 204941
benchmark_helloworld_generator/64 6257 ns 6255 ns 113871
benchmark_helloworld_generator/100 9176 ns 9173 ns 80898
Increasing the number of repeated strings increases the execution time. And actually, that was expected. 😂
Final remarks
Setting up a new project using Bazel is pretty straightforward. I really love the experience where you just add dependency and tooling does the rest for you, no need to download it or compile, etc. It simply worked with Google Test and Google Benchmark frameworks, but I am unsure how easy it is to integrate other libraries. One issue I came across was when I was trying to bump the Googe Test version to something more recent. The recent version requires at least C++14. Even though I set the language version correctly, Bazel was not able to pass this flag to external dependency and compilation failed.
GitHub - jakub-k-slys/bazel-cpp-sandbox