A header-only, real-time-conscious C++20 CAN bus library for Linux.
rtcan is designed for applications where a CAN interface has to run
alongside deterministic, latency-sensitive control loops: vehicle ECUs,
industrial controllers, robotics platforms, and other embedded Linux
systems where "usually fast" is not good enough. It separates raw frame
I/O from message decoding, moves all steady-state work onto fixed-capacity
lock-free queues, and gives every lifecycle transition (startup, fault,
shutdown) an explicit, race-tested state machine instead of best-effort
threading.
Most CAN libraries either give you a thin SocketCAN wrapper (fast, but you
build the lifecycle, threading, and codec layer yourself) or a heavyweight
framework (safe, but full of allocations, exceptions, and virtual dispatch
in the hot path). rtcan targets the space between: a small, auditable
core with an explicit concurrency contract, built so a strict real-time
consumer can opt into a compiler profile that removes exceptions, RTTI, and
non-lock-free atomics at compile time — while a generic Linux application
can use the same API with none of those restrictions.
- Zero-allocation hot path.
send(),update(), and the RX/TX/consumer worker loops use fixed-capacity SPSC queues; no heap allocation, no virtual dispatch, no exceptions once workers are running. - Explicit lifecycle, not best-effort threading.
Constructed -> Open -> Starting -> Running -> Stopping -> Stopped, withFaultedreachable from any running state. A worker fault can never be silently overwritten by a later "success" state, and stop/close/recover always wait for already admitted work before touching shared resources. - Deadline-based periodic TX, not tick-drift scheduling: every periodic
slot tracks its own absolute
CLOCK_MONOTONICdeadline. - Compile-time custom codecs. Give
BasicCanEndpointa concrete codec type and the whole encode/decode/dispatch path is static — no runtime message registry, nostd::function. - A strict RT compiler profile you can opt into, not one forced on
every consumer:
rtcan::rt_profileadds-fno-exceptions -fno-rtti -fno-threadsafe-staticsand a compile-time guarantee that every hot-path atomic on the target platform is lock-free — unsupported platforms fail to compile instead of silently falling back to a lock. - Hardware-free testing.
MockCanBusand a SocketCAN backend share the sameICanBusinterface, so lifecycle, fault-injection, and concurrency tests run without a CAN interface.
flowchart TB
App["Application messages\n(EngineStatus, VehicleSpeed, custom types)"]
Codec["ICanCodec / compile-time CodecT"]
Frame["RawCanFrame\n(fixed size, trivially copyable)"]
Bus["ICanBus"]
Socket["SocketCanBus"]
Mock["MockCanBus"]
App --> Codec --> Frame --> Bus
Bus --> Socket
Bus --> Mock
subgraph Endpoint["BasicCanEndpoint"]
RX["RX worker\n(bounded poll + receive)"]
Q["SPSC queue"]
Consumer["Consumer worker\n(decode + callback)"]
TX["TX worker\n(deadline scheduler + immediate queue)"]
end
Bus -.-> RX --> Q --> Consumer
TX -.-> Bus
Raw I/O and message decoding run on separate threads connected by a fixed-capacity SPSC queue, so a slow application callback cannot stall frame reception, and neither RX nor TX ever blocks on the other.
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failureThe default build is a portable generic profile: optimization, exceptions, RTTI, and CPU architecture choices are left to the CMake build type and the consuming application. Sanitizers are off by default.
# Package-only build: compiles no executable, only the header-only target
cmake -S . -B build-minimal \
-DRTCAN_BUILD_TESTS=OFF -DRTCAN_BUILD_EXAMPLES=OFF -DRTCAN_BUILD_APPS=OFF
cmake --build build-minimal
# Opt-in strict real-time compiler profile
cmake -S . -B build-rt -DCMAKE_BUILD_TYPE=Release -DRTCAN_ENABLE_RT_FLAGS=ON
cmake --build build-rtcmake --install build --prefix /opt/rtcanfind_package(rtcan 0.3 CONFIG REQUIRED)
target_link_libraries(my_consumer PRIVATE rtcan::rtcan)
# Optional: opt into the strict RT compiler profile
target_link_libraries(my_consumer PRIVATE rtcan::rtcan rtcan::rt_profile)#include <rtcan/rtcan.hpp>
void on_message(const rtcan::DecodedMessage& msg, void*) {
if (const auto* engine = std::get_if<rtcan::EngineStatus>(&msg)) {
// engine->rpm, engine->coolant_c, engine->throttle_pct
}
}
int main() {
rtcan::CanEndpoint endpoint("can0", /*bus_id=*/0, on_message, nullptr);
if (!endpoint.open()) return 1;
const auto handle = endpoint.add_periodic(
rtcan::DecodedMessage{rtcan::EngineStatus{.rpm = 1500.0f}},
/*period_ns=*/20'000'000);
const rtcan::StartResult result = endpoint.start();
if (!result.started) return 1;
endpoint.update(handle, rtcan::DecodedMessage{rtcan::EngineStatus{.rpm = 1800.0f}});
const rtcan::EndpointSnapshot view = endpoint.snapshot();
// view.endpoint_state, view.bus_health, view.tx_sent, ...
endpoint.stop();
endpoint.close();
}A compile-time custom codec, hardware-free examples, and a full
SocketCAN application skeleton are in examples/.
rtcan guarantees its own hot path is allocation-free, lock-free, and
(under the strict profile) exception/RTTI-free. It does not silently
change process-wide scheduling policy: SCHED_FIFO/SCHED_OTHER, CPU
affinity, mlockall, and capability configuration (CAP_SYS_NICE,
CAP_IPC_LOCK) remain explicit deployment decisions, made through
RtConfig/ThreadConfig or the surrounding environment.
The strict profile rejects unsupported platforms at compile time rather than falling back to a locking atomic:
target_link_libraries(my_consumer PRIVATE rtcan::rtcan rtcan::rt_profile)Application code stays responsible for the parts that are inherently
deployment-specific: CPU isolation (isolcpus, nohz_full), IRQ affinity,
RT throttling policy, and — for a hard real-time claim — measuring on the
actual target hardware. See Project status below.
The test suite runs the same source tree through five independent profiles in CI:
| Profile | What it proves |
|---|---|
| Debug + AddressSanitizer/UBSan | No memory errors, no undefined behavior |
| Release | Correctness under optimization |
| Strict RT profile | Compiles and passes with exceptions/RTTI removed; strict-atomic guard rejects non-lock-free platforms |
| ThreadSanitizer | No data races across RX/consumer/TX worker threads and lifecycle transitions |
required-vcan |
Real SocketCAN integration on a kernel vcan interface, no skips allowed |
Concurrency-sensitive behavior — second-start ownership, fault-vs-stop races, admitted-work accounting during shutdown, worker-context stop requests — is exercised with deterministic barrier tests (atomic wait/notify handshakes), not timing-dependent sleeps.
# Full suite
ctest --test-dir build --output-on-failure
# ThreadSanitizer only (matches the CI job)
cmake -S . -B build-tsan -DRTCAN_ENABLE_SANITIZERS=OFF -DRTCAN_ENABLE_TSAN=ON
cmake --build build-tsan
setarch "$(uname -m)" -R ctest --test-dir build-tsan -L tsan --output-on-failurertcan has gone through several rounds of independent, adversarial
concurrency review — each round re-verifying the previous round's fixes
against a fresh build rather than trusting prior claims, and deliberately
hunting for new races in whatever code the last round touched.
- Generic Linux: release-candidate quality. The full sanitizer, strict RT, and concurrency test matrix passes cleanly, including ThreadSanitizer runs with no filtering.
- Soft real-time: the architecture and lifecycle contracts described above are in place and tested; this is not yet backed by a signed, versioned release artifact from a remote CI run.
- Hard real-time: source- and binary-level guarantees (no allocation,
no exceptions/RTTI, lock-free atomics) are established and verified.
Target-platform qualification —
cyclictestlatency distribution, page-fault-free steady state, a 24-hour soak on an authorized PREEMPT_RT host with a defined deadline budget — has not been performed and is not claimed. A generic development machine cannot stand in for that measurement, so this repository does not claim it.
include/rtcan/ Public headers (header-only library)
examples/ Standalone demo programs
apps/ End-to-end SocketCAN application skeleton
tests/ ctest suite: unit, lifecycle, concurrency, fault-injection
src/ Reserved for future non-header-only sources
cmake/ Package export/config helpers
MIT — see LICENSE.