Someone asked me recently whether they could write a Linux kernel module in Python, because that’s the only language they knew well. The honest answer was no — not directly, anyway — and explaining why turned into a much longer conversation about what actually constrains language choice at the kernel level. It’s not really about taste or productivity the way it is in application development. It’s about what a language can guarantee (or refuse to guarantee) about memory, runtime behavior, and control over hardware.
Here’s a full rundown of what’s actually used, why, and where the exceptions live.
The Default: C
C is, overwhelmingly, the language kernel modules are written in — on Linux, on Windows (for the vast majority of legacy and many current drivers), and historically on most UNIX variants. There are specific, practical reasons for this, not just tradition:
- No hidden runtime. C doesn’t require a garbage collector, exception-handling runtime, or standard library initialization to function. The kernel is the runtime; there’s nothing underneath it to lean on.
- Deterministic memory control. Kernel code frequently needs to know exactly when memory is allocated and freed, because getting this wrong in a context where interrupts might be disabled, or where sleeping isn’t allowed, can crash the entire machine — not just one process.
- Direct hardware access. C allows pointer arithmetic, memory-mapped I/O, and inline assembly without an abstraction layer standing in the way.
- The kernel itself is written in C. A module has to link against and interoperate with kernel APIs that are all C functions and C structs, so writing the module in C avoids any calling-convention or ABI friction.
A minimal Linux example:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello from the kernel!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye from the kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
Windows drivers written in the WDM/KMDF model are also predominantly C (with some C++ used more loosely for structure, especially in older WDM code and newer UMDF user-mode drivers).
C++ — Used, But Carefully
C++ shows up in kernel-adjacent code more than people expect, but almost never in the “full modern C++ with exceptions and STL containers” form.
- Apple’s XNU kernel and IOKit are the strongest example: IOKit is explicitly object-oriented and implemented in a restricted subset of C++ (no exceptions, no RTTI, custom memory management), because Apple decided the abstraction benefits of classes and polymorphism for representing a hierarchy of devices and drivers outweighed the overhead, as long as the riskiest C++ features were disabled.
- Windows allows C++ in kernel-mode drivers (particularly with the KMDF C++ wrappers some teams use), again typically avoiding exceptions and RTTI, since unwinding an exception through kernel stack frames without a supporting runtime is dangerous.
- Linux technically can be built with limited C++ support in a few kernel subsystems and out-of-tree efforts have experimented with it, but mainline Linux module development in C++ isn’t standard practice and isn’t well supported by the module build system.
The recurring theme: where C++ is used in kernel space, it’s a deliberately restricted dialect — no exceptions, no garbage collection assumptions, careful control over object construction/destruction timing.
Rust — The Newest Serious Entrant
Rust has moved from “interesting experiment” to “officially supported” in the Linux kernel over the past few years. As of recent kernel releases, Rust is an accepted language for writing certain kernel modules and drivers, with infrastructure (rust/ directory, bindings generation, a subset of the language considered safe for kernel use) merged into mainline.
The appeal is specific: Rust’s ownership and borrow-checking system can catch entire classes of bugs — use-after-free, double-free, certain data races — at compile time, which is exactly the category of bug that causes the worst kernel crashes and security vulnerabilities in C code. Early adopters have included Android’s binary GPU driver work, some network drivers, and filesystem experiments (like parts of the Btrfs-adjacent tooling and new filesystem prototypes).
// Simplified illustrative shape of a Rust kernel module (not full working code)
use kernel::prelude::*;
module! {
type: MyModule,
name: "my_module",
author: "Example",
description: "A minimal Rust kernel module",
license: "GPL",
}
struct MyModule;
impl kernel::Module for MyModule {
fn init(_module: &'static ThisModule) -> Result<Self> {
pr_info!("Hello from a Rust kernel module!\n");
Ok(MyModule)
}
}
Rust in the kernel still runs without its normal standard library (no_std), without unwinding-based panics propagating freely, and with an explicit unsafe boundary whenever it has to call into existing C kernel APIs — because the kernel’s C side offers no safety guarantees Rust can verify.
Assembly
Every kernel — Linux, Windows, XNU, and others — has small amounts of hand-written assembly, and it’s fair to call this a “language used for kernel modules” in a narrow sense. It shows up in:
- Architecture-specific boot and context-switch code
- Highly performance-critical routines (certain cryptographic primitives, spinlock implementations)
- Places where a specific CPU instruction has no C equivalent (privileged instructions, certain atomic operations, interrupt vector table entries)
Ordinary module authors essentially never need to write assembly directly — it’s mostly confined to the deepest layers of the kernel and to specific driver code needing precise instruction-level control.
What About Higher-Level Languages?
This is where the honest “no” comes back in for languages like Python, Java, Go, or JavaScript, and it’s worth explaining precisely why:
- Garbage collection. These languages assume a background process can pause execution and reclaim memory. The kernel doesn’t have a concept of “pause everything to garbage collect” that’s safe in arbitrary contexts, especially interrupt handlers or code holding spinlocks.
- Runtime and standard library dependencies. Python needs an interpreter; the JVM needs, well, a JVM. Neither can bootstrap itself as the very foundation the OS runs on.
- Non-deterministic latency. Kernel code sometimes has hard timing constraints (interrupt handlers need to complete quickly); a GC pause at the wrong moment is unacceptable.
That said, there are indirect ways these languages interact with kernel-level functionality:
- eBPF lets you write small, verified programs (commonly authored in a restricted C subset, though tooling exists to generate eBPF bytecode from higher-level frontends) that get loaded into the kernel and JIT-compiled, running in a sandboxed, verified environment rather than as an ordinary module. Projects like
bccandbpftracelet you write the tooling around eBPF in Python or a custom scripting language, while the actual in-kernel logic stays restricted. - FUSE (Filesystem in Userspace) lets you implement filesystem logic in almost any language — Python, Go, Rust, whatever — because the actual code runs in user space, and only a thin, already-written kernel module handles the low-level VFS interaction, forwarding requests to your user-space process.
- Character/network device logic can be split so that a minimal C or Rust kernel module handles the privileged parts, while a user-space daemon (in any language) handles higher-level logic, communicating over
ioctl, netlink sockets, or a custom/proc//sysinterface.
Comparison Table
| Language | Used In-Kernel? | Typical Role | Key Constraint |
|---|---|---|---|
| C | Yes, dominant | Nearly all Linux/Windows drivers | No safety net; manual memory management |
| C++ | Yes, restricted | IOKit (macOS/iOS), some Windows drivers | No exceptions/RTTI typically |
| Rust | Yes, growing | New Linux drivers, some Android components | unsafe boundary to C APIs |
| Assembly | Yes, minimal | Boot code, context switches, crypto primitives | Architecture-specific, low-level only |
| Python/Go/Java/JS | No (not directly) | Tooling around eBPF, FUSE daemons, user-space helpers | Requires a runtime the kernel can’t provide |
Real-World Examples by Platform
- Linux: virtually all mainline drivers in C; growing Rust adoption (Android binder-adjacent work, Asahi Linux’s GPU driver, Nova Nvidia driver effort); eBPF programs for networking/observability written in restricted C.
- Windows: WDM/KMDF drivers in C, some C++; newer UMDF (user-mode) drivers can use a broader range of practices since they run outside kernel space.
- macOS/iOS: IOKit drivers historically in restricted C++; Apple has been pushing third-party developers toward DriverKit, which runs drivers in user space specifically to reduce the risk surface of kernel-mode code altogether.
- Android: Linux kernel underneath, so C and increasingly Rust; most app-facing “driver” work is actually in user space via HALs (Hardware Abstraction Layers), which can be written in C++ or Java/Kotlin since they don’t run in kernel context.
Troubleshooting Tips for Language-Related Kernel Module Issues
- Compiler/kernel version mismatches are the most common C module build failure — always build against the exact kernel headers of the running kernel (
/lib/modules/$(uname -r)/build). - Rust module builds require a specific pinned Rust toolchain version matching what the kernel tree expects; mismatches produce cryptic build failures.
- C++ kernel code failing to link often traces back to accidentally pulling in standard library features (exceptions, RTTI) that the kernel build explicitly disables.
Best Practices
- Default to C unless you have a specific, well-justified reason not to — tooling, documentation, and community support are all strongest there.
- If pursuing Rust, use it for new, self-contained drivers rather than trying to port large existing C subsystems, and lean on the safe abstractions the kernel Rust project provides instead of writing
unsafeblocks freely. - Push logic that doesn’t need kernel privileges out to user space (via FUSE, UMDF, DriverKit, or a helper daemon) whenever possible — smaller kernel-mode code surfaces mean fewer catastrophic bugs.
- Never try to force a garbage-collected language into kernel space directly; use eBPF or a user-space helper process instead.
A Closer Look at Why C Won and Kept Winning
It’s worth spending a bit more time on why C specifically became the near-universal choice, because the reasons go beyond “it’s what the kernel is written in already.”
Predictable code generation. C compilers, especially GCC and Clang as used for kernel builds, produce code whose behavior is close to what’s written — there’s no hidden allocation, no implicit function calls inserted by the compiler for things like operator overloading or automatic boxing/unboxing. When you’re writing code that might run in an interrupt handler with a few microseconds of budget, that predictability isn’t a nice-to-have, it’s a requirement.
Structural fit with the existing ABI. The kernel’s calling conventions, structure layouts, and symbol export mechanisms are all defined in terms of C’s type system. Any other language wanting to interoperate has to either compile down to something C-compatible at the ABI level or maintain an explicit translation layer — which is exactly what the Rust-for-Linux project had to build (a bindgen-based bridge generating Rust-callable wrappers around the kernel’s C headers).
Decades of tooling investment. Static analyzers (Sparse, Coccinelle, Smatch), debuggers (kgdb, crash utility), and tracing infrastructure (ftrace, kprobes) are all deeply tuned for C kernel code specifically. A new language entering this space has to either integrate with that tooling or slowly rebuild equivalent tooling of its own — a real, multi-year cost that partly explains why even a well-funded, well-motivated effort like Rust-for-Linux took years to reach mainline inclusion, and why it started with new, self-contained drivers rather than a wholesale rewrite of existing subsystems.
Language Choice in Specialized Kernel-Adjacent Contexts
Beyond the “can this be a loadable module” question, it’s worth noting how language choice shifts once you move slightly outside strict kernel-module territory:
- Bootloaders (GRUB, U-Boot) are typically written in a mix of C and architecture-specific assembly, since they run in an even more constrained environment than the kernel itself — sometimes without any memory management infrastructure at all yet.
- Hypervisors like Xen are largely C, with some components (particularly in newer projects) exploring Rust for the same memory-safety motivations driving Rust-for-Linux.
- Real-time and safety-critical embedded kernels (used in automotive and aerospace contexts) sometimes use heavily restricted C dialects (MISRA C being the best-known standard) specifically to eliminate undefined-behavior-prone constructs, rather than switching languages entirely — a middle path between “plain C” and “a memory-safe language,” reflecting how conservative these industries tend to be about kernel-level language choices.
- Formally verified microkernels like seL4 take yet another approach: the kernel itself is written in C, but that C implementation is mathematically proven, using a separate formal specification and proof toolchain (largely in the Isabelle/HOL theorem prover), to match a higher-level specification exactly — sidestepping the “which language is inherently safer” question by instead proving the existing C implementation correct.
What This Means for Someone Choosing a Path Into Kernel Development
If you’re deciding where to invest learning time, the practical answer today looks like this: learn C first, because it remains the language the overwhelming majority of kernel code, documentation, tooling, and community expertise assumes. Once comfortable there, Rust is a genuinely valuable second skill, particularly if new driver development or memory-safety-focused subsystems interest you, and its official mainline status means that investment isn’t speculative the way it might have felt five years ago. For anyone whose real interest is in devices and hardware but who doesn’t want to work at kernel-privilege level at all, user-space frameworks (FUSE, UMDF, DriverKit) offer a genuinely productive path using languages far outside the traditional kernel toolkit.
Summary
Kernel modules are, and will likely remain for a long time, primarily a C endeavor, because C offers exactly the low-level control and absence of hidden runtime behavior that kernel code requires. C++ appears in a restricted form where object-oriented driver models make real sense (IOKit being the standout example). Rust is the most significant recent shift, bringing compile-time memory safety guarantees into a space that has historically been extremely bug-prone, and it’s now an officially supported option in mainline Linux. Assembly remains confined to the lowest-level, most hardware-specific corners. Higher-level languages like Python, Go, or Java simply can’t run as true kernel modules because they depend on runtime infrastructure the kernel can’t provide — but they thrive in the user-space and tooling layers that surround kernel functionality, especially through mechanisms like eBPF and FUSE that were purpose-built to bridge that gap.
FAQs
Can I write a Linux kernel module in Python? Not directly as a loadable kernel module — Python needs an interpreter runtime the kernel can’t host. You can, however, write user-space tooling around kernel features like eBPF in Python.
Is Rust actually used in production kernels today? Yes — Rust support has been merged into mainline Linux, and it’s used in specific drivers and subsystems, though C still dominates by a wide margin.
Why doesn’t the kernel just use C++ everywhere for better abstractions? Full C++ assumes runtime support (exception handling, RTTI) that’s risky or unavailable at the kernel level; where C++ is used, it’s a deliberately restricted subset.
What’s the safest way to add custom logic without touching kernel-mode code at all? FUSE for filesystems, UMDF/DriverKit for many device driver categories, and eBPF for observability/networking logic — all let you avoid full kernel-mode programming.
Do I need assembly to write a typical driver? No — the overwhelming majority of driver code is portable C; assembly is reserved for the deepest architecture-specific layers.
References
- Linux Kernel Documentation, “Rust for Linux” — https://docs.kernel.org/rust/
- Linux Kernel Documentation, “A Guide to the Kernel Development Process” — https://www.kernel.org/doc/html/latest/process/development-process.html
- Apple Developer Documentation, “IOKit Fundamentals” — https://developer.apple.com/documentation/kernel/iokit
- Apple Developer Documentation, “DriverKit” — https://developer.apple.com/documentation/driverkit
- Microsoft Docs, “Write a Windows Driver Framework driver based on a template” — https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/
- eBPF Documentation — https://ebpf.io/what-is-ebpf/
