What programming languages are commonly used for writing kernel modules

What programming languages are commonly used for writing kernel modules

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:

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.

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:

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:

That said, there are indirect ways these languages interact with kernel-level functionality:

Comparison Table

LanguageUsed In-Kernel?Typical RoleKey Constraint
CYes, dominantNearly all Linux/Windows driversNo safety net; manual memory management
C++Yes, restrictedIOKit (macOS/iOS), some Windows driversNo exceptions/RTTI typically
RustYes, growingNew Linux drivers, some Android componentsunsafe boundary to C APIs
AssemblyYes, minimalBoot code, context switches, crypto primitivesArchitecture-specific, low-level only
Python/Go/Java/JSNo (not directly)Tooling around eBPF, FUSE daemons, user-space helpersRequires a runtime the kernel can’t provide

Real-World Examples by Platform

Troubleshooting Tips for Language-Related Kernel Module Issues

Best Practices

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:

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

Exit mobile version