100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

D and C Interop

How D's native extern(C) linkage lets you call existing C libraries directly and expose D functions to C code without writing wrapper glue.

Practical DIntermediate9 min readJul 10, 2026
Analogies

Why D Interoperates with C So Easily

D was designed with C ABI compatibility as a first-class goal: an extern(C) function uses the platform's standard C calling convention and skips D's name mangling entirely, so it links directly against object code compiled from real C sources -- static .a/.lib archives or shared .so/.dll libraries -- with zero wrapper or shim code required.

🏏

Cricket analogy: When Virat Kohli moves from IPL colors straight into national team whites, no retraining is needed because both use the same bat, pitch, and scoring rules; D's extern(C) functions plug straight into existing C object code the same way, because both share the same calling convention.

Declaring C Functions and extern(C)

To call a C function from D, you declare its prototype with extern(C) linkage matching the original C signature exactly, then link against the compiled library using dmd/ldc2's -L flags or dub's 'libs' setting; the core.stdc.* modules already provide extern(C) declarations for the C standard library, so you rarely need to hand-write raw declarations for common functions like printf, malloc, or strlen.

🏏

Cricket analogy: Before facing a fast bowler like Jasprit Bumrah, a batsman studies his exact release point and seam position to predict the delivery; declaring an extern(C) prototype in D is that same study, writing down the C function's exact signature so the linker knows precisely what to expect.

d
// mathinterop.d
import core.stdc.stdio : printf;

extern(C) double sqrt(double x); // matches C's <math.h> sqrt

void main() {
    double result = sqrt(2.0);
    printf("sqrt(2.0) = %f\n", result);
}

// Compile and link against libm:
//   dmd mathinterop.d -L-lm

Matching Struct Layout Between D and C

When exchanging structs across the interop boundary, D's default struct layout already follows the platform's C ABI rules for field ordering, size, and alignment, so a D struct with the same field types and order as a C struct is binary-compatible without any special attribute; if the C side used #pragma pack(1) or __attribute__((packed)), you must add D's align(1) to the struct definition or the field offsets will silently diverge and corrupt data read across the boundary.

🏏

Cricket analogy: A pitch curator measures grass length and soil compaction to match the exact spec used at another venue so a day-night Test plays consistently; matching D struct layout to a C struct's packing is that same precise spec-matching, since a mismatched pitch shifts every subsequent measurement.

D's garbage collector does not know about memory allocated by C's malloc, and C code has no idea about D's GC-managed heap. If you pass a GC-allocated D array or class instance into a C function that stores the pointer for later use, the GC may collect or move that memory once no D-visible reference remains, corrupting the C side. Use core.memory.GC.addRoot for anything that must outlive the collector's visibility, or allocate with core.stdc.stdlib.malloc instead, and always call GC.removeRoot when done.

Calling D Functions from C and Building Shared Libraries

To expose a D function to C, mark it extern(C) so the compiler emits an unmangled symbol name, compile it into a static or shared library (or use dub's 'dynamicLibrary' targetType), and declare a matching prototype in the C header; the D runtime must be initialized first via rt_init()/rt_term() -- and thread_attachThis()/thread_detachThis() for callbacks arriving on C-created threads -- whenever any D-side GC, exceptions, or class features are touched, since C has no notion of the D runtime.

🏏

Cricket analogy: When a franchise signs an overseas player like Pat Cummins for the IPL, he must first register with the league and clear visa formalities before taking the field; a D function called from C needs the runtime initialized with rt_init() before it can safely take the field.

The dpp tool and druntime's core.stdc.* modules can auto-translate C headers into extern(C) D declarations, saving you from hand-transcribing large libraries; for C++ headers, D's extern(C++) linkage additionally preserves name mangling and class layout, letting you call C++ methods more directly than through a plain C shim.

  • extern(C) linkage removes D's name mangling so functions link directly against compiled C object code.
  • core.stdc.* modules already declare most C standard library functions; avoid re-declaring them by hand.
  • D struct layout matches C's by default; use align() to mirror any #pragma pack in the C struct.
  • The D garbage collector cannot see C-allocated memory, and C code can't see D's GC heap -- root anything shared.
  • Exposing a D function to C requires extern(C) linkage plus a matching C prototype in a header.
  • A C host calling into GC- or exception-touching D code must call rt_init() (and rt_term() at shutdown) first.
  • extern(C++) exists separately from extern(C) for interoperating with C++ symbols and class layouts.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#DProgrammingStudyNotes#DAndCInterop#Interop#Interoperates#Easily#Declaring#StudyNotes#SkillVeris#ExamPrep