Skip to main content
  1. Articles/

The Case for C on New Embedded Projects in 2026

·9 mins
Daniel Miess
Author
Daniel Miess
Embedded systems, telematics, and low-level software. Writing C for ARM Linux devices.

Every couple of months someone asks me why we are writing C for a brand new embedded platform in 2026. The question is fair. Rust has memory safety. Go has goroutines and a standard library that makes networking trivial. Modern C++ has smart pointers and RAII. C has malloc and good luck.

I get the skepticism. But after working in C across multiple embedded Linux platforms — cellular gateways, display drivers, telematics devices — I keep coming back to it for new projects, not out of habit, but because the practical realities of embedded development still favor it in ways that are hard to appreciate until you have shipped firmware on constrained hardware.

This is not a language war piece. I have used C++ extensively and liked it. I think Rust is genuinely interesting. But the decision about what language to write production firmware in is not an abstract one, and the factors that matter most are not the ones that get discussed in blog posts.

The Ecosystem Is the Language
#

When people compare languages, they compare syntax and features. When I pick a language for an embedded project, I am mostly thinking about the ecosystem.

C ecosystem for embedded Linux: kernel APIs, vendor SDKs, debugging tools, and cross-compilation all assume C

Every vendor SDK I have ever worked with is in C. Qualcomm, Sierra Wireless, NXP — the reference code, the API headers, the example applications, all C. The Linux kernel interfaces are C. ModemManager and NetworkManager expose D-Bus APIs, but their plugin interfaces and internal APIs are GObject-based C. When you need to write a custom ModemManager plugin to support a new modem chipset, you are writing C whether you planned to or not.

You can call C from other languages. FFI exists. But FFI is a boundary, and every boundary is a place where bugs hide. Wrapping a vendor SDK that passes around raw pointers and expects you to manage buffer lifetimes in a language whose whole selling point is safe memory management creates friction. You spend time writing bindings instead of writing the actual application. For a small team shipping firmware on a deadline, that overhead matters.

The practical reality is that in embedded Linux, C is not just a language. It is the lingua franca that everything else is built on. Choosing C means you can use vendor code directly, contribute patches upstream without translation, and read kernel source to debug problems without a language barrier between you and the system.

Debugging on Target
#

This is the one that matters most to me day to day, and it gets overlooked in language comparisons.

When a device in the field does something unexpected, I need to reproduce it and debug it. On an ARM64 embedded board, that means cross-compiling, deploying to the device, attaching GDB over SSH, and stepping through code. This workflow with C is as good as it gets. GDB understands C natively. The mapping between source lines and instructions is direct. Stack traces are readable. Memory layouts are predictable. When I inspect a struct, I see the struct. When I set a breakpoint, I know exactly what code is going to execute.

Debugging workflow comparison: C gives direct GDB access with transparent state, while languages with runtimes add opacity

With garbage-collected languages, the debugging story on embedded targets gets murkier. The runtime is doing things you did not ask for — garbage collection pauses, goroutine scheduling, heap compaction. These are fine on a server. On an embedded device where you are trying to figure out why a modem AT command timed out after exactly 3.2 seconds, runtime non-determinism is a problem. You need to know that between point A and point B in your code, nothing else happened. C gives you that.

Rust’s debugging story is better than Go’s for embedded — no garbage collector, predictable layout — but GDB support for Rust is still catching up. Complex Rust types like enums, iterators, and trait objects can produce debug info that GDB does not display cleanly. It is improving, but “improving” is not the same as “works reliably today on the ARM target board sitting on my desk.”

I have lost enough time to tooling friction that I weight this heavily. The language you debug in matters more than the language you write in, because debugging is where you spend time when things go wrong, and things always go wrong.

Cross-Compilation Is a Solved Problem
#

Embedded development means cross-compilation. I write code on an x86 workstation and it runs on an ARM64 device. With C, the cross-compilation toolchain is mature to the point of being boring. aarch64-linux-gnu-gcc works. It has worked for years. The Yocto build system, which is the standard for building embedded Linux images, is built entirely around C and C++ toolchains. CMake, Make, autotools — the build system ecosystem assumes C.

Cross-compiling Rust for embedded ARM targets works, but the experience is not as smooth. You need the right target triple, the right sysroot, and if you depend on crates that have C dependencies (which many do), you are back to dealing with a C cross-compilation toolchain anyway, plus the Rust one on top. It is an extra layer.

Go cross-compiles easily for the simple case, but CGo — which you need the moment you want to call into a C library — introduces the same cross-compilation complexity as C, with the added complication that the Go and C toolchains need to agree on linking.

None of these are unsolvable problems. But in embedded development, the build system is already one of the most frustrating parts of the job. Adding complexity to it for a language choice has a real cost.

Binary Size and Runtime Overhead
#

On a server, nobody cares if your binary is 2MB or 20MB. On an embedded device with limited flash storage and a firmware image that ships over cellular connections, it matters.

A C program links against libc, which is already on the device, and produces a small binary. A Go program bundles its entire runtime, including the garbage collector and goroutine scheduler. For a simple application, the Go binary can be 5-10x larger than the C equivalent. Rust is closer to C in binary size, but still tends to be somewhat larger due to monomorphization and the standard library.

Runtime memory overhead follows a similar pattern. A C program uses what you allocate. A Go program needs memory for its runtime, its garbage collector, and its goroutine stacks. On a device where total RAM is measured in hundreds of megabytes and shared among multiple processes, the runtime overhead of higher-level languages eats into your budget.

I have worked on devices where we carefully tracked memory usage per process because we had to. In that environment, a language that uses memory you did not explicitly ask for is a liability.

The Hiring Question
#

This one is less technical but equally real. When I need to hire an embedded developer, I need someone who understands pointers, memory layouts, hardware registers, and the Linux kernel interfaces. That person almost certainly knows C. They might also know C++ or Rust, but C is the baseline.

The pool of embedded developers who are productive in Rust is growing, but it is still small. Finding someone who can write safe, idiomatic Rust and understands the embedded domain — kernel interfaces, device trees, cross-compilation, hardware debugging — is hard. Finding someone who can do that in C is not easy either, but the pool is significantly larger.

For a team that needs to ship firmware and will need to hire people to maintain it, language choice is also a staffing decision. Picking a language that a small fraction of your candidate pool knows is a risk.

Where C Is the Wrong Choice
#

I do not think C is the right answer everywhere, even in embedded.

Comparison of C, Rust, and Go across embedded development criteria

Network-facing parsers and protocol implementations are where C’s weaknesses hurt the most. Parsing untrusted input with manual buffer management is where buffer overflows live. If I were writing a new TLS implementation or a protocol parser that handles data from the network, I would want Rust’s guarantees. The cost of a memory safety bug in code that faces the internet is too high.

Higher-level application logic that does not touch hardware or kernel interfaces is a reasonable place for Go or Rust. If you have a component that reads from a message queue, transforms some data, and sends it somewhere — and it does not need to interact with C libraries or vendor SDKs — then the ergonomic benefits of a higher-level language might be worth it.

New projects with no vendor SDK dependency and a team that already knows Rust are a legitimate case for Rust on embedded. The language’s safety properties are real, and if you are not fighting the ecosystem to use it, the trade-off shifts in its favor.

The key word is “trade-off.” There is no free lunch. Rust’s safety comes with ecosystem friction and a steeper learning curve. Go’s productivity comes with runtime overhead and a garbage collector. C’s ecosystem fit comes with the responsibility of managing your own memory. The right choice depends on the specific constraints of the project, not on which language won the latest internet argument.

What I Actually Do
#

On the platform I work on now — an ARM64 Linux telematics device — the firmware is C. The decision was made before I joined, but I agree with it. We interface with ModemManager, NetworkManager, and custom kernel modules. We talk to hardware over memory-mapped registers. We cross-compile with Yocto. We debug on target with GDB over SSH.

Every one of those activities is easier in C than it would be in any alternative. Not because C is a better language in the abstract, but because the entire toolchain, ecosystem, and workflow assumes C. Fighting that assumption would cost us time we do not have, for benefits that would not show up in the reliability of the shipped product.

C in 2026 is not exciting. It does not have a type system that catches use-after-free at compile time. It will not generate conference talks about how you rewrote everything and it was better. But it works, the tools are mature, the ecosystem is deep, and when something goes wrong at 3am on a device in a truck somewhere, I can attach a debugger and see exactly what is happening.

For the kind of embedded work I do, that is what matters.

Related