Bazel (C / C++)¶
Bazel through rules_dsdl, which offers two ways to get generated code into a build: generate at fetch time as an external repository, or generate as build actions. They trade against each other rather than one superseding the other, and this recipe builds both.
| Language | C |
| Build system | Bazel |
| Idiom | B -- your project owns the manifest |
| Regeneration | None -- regeneration is an explicit step |
Notes¶
The choice is when generation happens, and everything else follows.
Bazel builds in three phases: it loads BUILD files and fetches external repositories, then analyses them into a complete graph of every command it will run -- with every input and output named -- and only then executes. Nothing discovered during execution can change the plan.
| Path A: fetch time | Path B: build actions | |
|---|---|---|
| Runs during | Loading | Execution |
| Outputs known by | glob, after generation |
Declaration, before anything runs |
| Standard types the definitions reach | Handled automatically | Listed by hand |
| Sandboxed | No | Yes |
| Action cache / remote cache | Whole-repository refetch | Per generated file |
| Remote execution | No, always local | Yes |
Path A — dsdlc.namespace in MODULE.bazel. A repository rule runs before any BUILD file is read, so the generated files are on disk by the time Bazel evaluates the glob over them. Nothing is declared in advance and so nothing can be omitted. That matters because a definition's dependency closure is invisible in its filename: nothing in 6210.VehicleState.1.0.dsdl says it reaches three different Vector3 types, and lanyard reaches twelve standard types in all.
Correctness rests on watch(), over both inputs. The rule asks dsdlc which definitions it reads and watches every one, so editing a .dsdl refetches and regenerates; it also watches dsdlc itself, so rebuilding or upgrading the compiler in place does too. Neither is an action input here, and what Bazel is not told to watch it cannot see -- without the first, the build quietly serves code that no longer matches the schema; without the second, a dsdlc that cannot even run still reports up-to-date. Under Path B both come free: the definitions and the compiler are ordinary action inputs, and Bazel digests them.
Path B — the macros. Generation becomes a genrule in the action graph, which is what buys sandboxing, per-file action caching, and remote execution. The price is the declaration: output names are derivable from input names, but the closure is not, so dsdl_builtin_library needs the standard types spelled out. Miss one and the build stops on an unresolved include for a type you never wrote.
Choosing. Take Path B if you want codegen to be a first-class citizen of Bazel's execution machinery -- a large monorepo on remote execution has a real reason to. Take Path A otherwise, and especially as the namespace grows, because it is the one that does not ask you to maintain a list that changes whenever a definition gains a field.
dsdlc is found as a host tool by a repository rule reading DSDLC or falling back to PATH, the same shape Bazel uses for a C compiler -- an action could not do that, which is why dsdlc has to be an external repository before a genrule can name it as a tool. .bazelversion pins Bazel to the version the toolshed image pre-warms; .bazelrc pins the system clang on macOS, an ordinary Bazel-on-macOS papercut unrelated to DSDL.
Prerequisites¶
| Tool | Why |
|---|---|
bazel |
the build system this recipe is about |
cc |
compiles the generated C and the round-trip program |
Commands¶
Run these from the recipe directory, with dsdl/ and src/ copied alongside it. This is the exact sequence CI runs.
# build
bazel build "--override_module=rules_dsdl=$LLVM_DSDL_PREFIX/share/rules_dsdl" //:roundtrip_fetched //:roundtrip_prebuilt //:roundtrip_actions
# round-trip (fetch-time)
./bazel-bin/roundtrip_fetched
# round-trip (prebuilt archive)
./bazel-bin/roundtrip_prebuilt
# round-trip (action-generated)
./bazel-bin/roundtrip_actions
The types this builds¶
The whole lanyard namespace -- twenty-four definitions, browsable from the showroom overview, where each one is paired with its wire-layout facts and a declaration excerpt in every language.
The build files¶
BUILD.bazel¶
load("@rules_cc//cc:defs.bzl", "cc_binary")
load("@rules_dsdl//:defs.bzl", "dsdl_builtin_library", "dsdl_c_library", "dsdl_support_library")
# rules_dsdl offers two ways to get generated code into a Bazel build. They are a real exchange
# rather than a good option and a worse one, and which suits you depends on what you want out of
# Bazel. Both are shown below, each self-contained; copy the half you want.
#
# The difference is *when* generation happens, and everything else follows from that. The recipe page
# has the full comparison.
# --------------------------------------------------------------------------------------------------
# Path A: generate at fetch time, with a namespace repository.
#
# Declared in MODULE.bazel, one line per namespace. The rule runs during Bazel's loading phase,
# before any BUILD file is read, so by the time the generated BUILD file is evaluated the files are
# already on disk and a glob can simply see them. Nothing is declared in advance, so nothing can be
# left out -- including the standard types the definitions reach, which no filename reveals.
#
# What you give up: generation is not an action, so it is not sandboxed, not action-cached per file,
# and not available to remote execution. The repository is refetched whole or not at all.
cc_binary(
name = "roundtrip_fetched",
srcs = ["src/c/roundtrip.c"],
deps = ["@lanyard//:lanyard"],
)
# The obj backend on the same path: the same one line in MODULE.bazel with `language = "obj"`. dsdlc
# publishes the headers beside the archive, so the generated library wraps a cc_import and carries
# the include path.
cc_binary(
name = "roundtrip_prebuilt",
srcs = ["src/c/roundtrip.c"],
deps = ["@lanyard_prebuilt//:lanyard_prebuilt"],
)
# --------------------------------------------------------------------------------------------------
# Path B: generate as build actions, with the macros.
#
# Generation becomes a genrule in the action graph: sandboxed, action-cached per output, and
# available to remote execution. That is the whole reason to choose it.
#
# What it costs: Bazel needs every output named during analysis, before anything runs. Output *names*
# are derivable from input names, so listing `srcs` covers the definitions themselves -- but their
# dependency closure is not derivable, so anything they reach must be generated by another target and
# named by hand. One entry here; twelve for a namespace of two dozen definitions. Leaving one out is
# a compile error naming a type you never wrote.
dsdl_support_library(name = "dsdl_support")
dsdl_builtin_library(
name = "dsdl_builtin",
types = [
"uavcan.diagnostic.Severity.1.0",
"uavcan.node.Health.1.0",
"uavcan.si.unit.angle.Quaternion.1.0",
"uavcan.si.unit.angle.Vector3.1.0",
"uavcan.si.unit.angular_velocity.Vector3.1.0",
"uavcan.si.unit.electric_current.Scalar.1.0",
"uavcan.si.unit.length.Scalar.1.0",
"uavcan.si.unit.length.WideScalar.1.0",
"uavcan.si.unit.temperature.Scalar.1.0",
"uavcan.si.unit.velocity.Vector3.1.0",
"uavcan.si.unit.voltage.Scalar.1.0",
"uavcan.time.SynchronizedTimestamp.1.0",
],
deps = [":dsdl_support"],
)
dsdl_c_library(
name = "lanyard_actions",
srcs = [
"dsdl/lanyard/flight/6210.VehicleState.1.0.dsdl",
"dsdl/lanyard/flight/6210.VehicleState.1.1.dsdl",
"dsdl/lanyard/flight/6211.VehicleState.2.0.dsdl",
"dsdl/lanyard/flight/6212.ControlSurfaces.1.0.dsdl",
"dsdl/lanyard/flight/FixedWingSurfaces.1.0.dsdl",
"dsdl/lanyard/flight/FlightMode.1.0.dsdl",
"dsdl/lanyard/flight/MultirotorMix.1.0.dsdl",
"dsdl/lanyard/health/258.LegacyBatteryPoll.1.0.dsdl",
"dsdl/lanyard/health/6250.SystemHealth.1.0.dsdl",
"dsdl/lanyard/health/6251.BatteryStatus.2.0.dsdl",
"dsdl/lanyard/health/SubsystemReport.1.0.dsdl",
"dsdl/lanyard/link/6240.RcInput.1.0.dsdl",
"dsdl/lanyard/link/6241.TelemetryLinkStats.1.0.dsdl",
"dsdl/lanyard/nav/256.UploadMission.1.0.dsdl",
"dsdl/lanyard/nav/6220.GlobalPosition.1.0.dsdl",
"dsdl/lanyard/nav/6221.MissionPlan.1.0.dsdl",
"dsdl/lanyard/nav/Waypoint.1.0.dsdl",
"dsdl/lanyard/payload/257.CapturePhoto.1.0.dsdl",
"dsdl/lanyard/payload/6230.GimbalStatus.1.0.dsdl",
"dsdl/lanyard/payload/6231.CameraFrameMetadata.1.0.dsdl",
"dsdl/lanyard/propulsion/6200.EscStatus.1.0.dsdl",
"dsdl/lanyard/propulsion/6201.EscStatus.2.0.dsdl",
"dsdl/lanyard/propulsion/6202.ThrottleCommand.0.1.dsdl",
"dsdl/lanyard/propulsion/6203.ThrottleCommand.1.0.dsdl",
],
root = "dsdl/lanyard",
deps = [
":dsdl_builtin",
":dsdl_support",
],
)
cc_binary(
name = "roundtrip_actions",
srcs = ["src/c/roundtrip.c"],
deps = [":lanyard_actions"],
)
MODULE.bazel¶
module(name = "lanyard_roundtrip", version = "0.1.0")
# Until rules_dsdl is published to a registry, the build command points Bazel at the copy installed
# alongside dsdlc with --override_module. See the commands on this recipe's page.
bazel_dep(name = "rules_dsdl", version = "0.1.0")
bazel_dep(name = "rules_cc", version = "0.1.1")
dsdlc = use_extension("@rules_dsdl//:extensions.bzl", "dsdlc")
# One line per namespace, and nothing to keep in step. Everything the namespace reaches -- including
# the standard types from the catalog compiled into dsdlc -- is generated at fetch time, so there is
# no list of transitive dependencies to maintain and no way to leave one out.
dsdlc.namespace(name = "lanyard", root = "dsdl/lanyard")
# The same definitions as a prebuilt archive. Same one line, different backend.
dsdlc.namespace(
name = "lanyard_prebuilt",
root = "dsdl/lanyard",
language = "obj",
options = ["--target-endianness", "little"],
)
use_repo(dsdlc, "dsdlc", "lanyard", "lanyard_prebuilt")
.bazelrc¶
# Bazel keeps a background server between invocations. Left at its default it lingers for hours
# after a one-shot build, which is wrong for CI and merely untidy for a developer running the
# recipe once. A short idle timeout retires it on its own -- preferable to an explicit
# `bazel shutdown`, which is one more thing that can hang waiting on a server it cannot kill.
startup --max_idle_secs=15
# dsdlc is a host tool, like a compiler: rules_dsdl locates it through the DSDLC environment
# variable, falling back to PATH. Both lines are needed and they are not redundant -- the repository
# rule that finds it runs in the repo phase, the genrule that runs it is an action.
common --repo_env=DSDLC
build --action_env=DSDLC
# Applies the build:macos / build:linux sections below according to the host.
common --enable_platform_specific_config
# Bazel's C toolchain autodetection on macOS can settle on a Homebrew gcc and then hand it the
# Apple-specific link flags it expects clang to understand, which fails with
# `unrecognized command-line option '-fobjc-link-runtime'`. Pinning the system clang avoids it.
# Nothing here is specific to DSDL; it is the ordinary Bazel-on-macOS papercut.
build:macos --repo_env=CC=/usr/bin/clang
.bazelversion¶
9.2.0