Libmir Archive

BetterC Guide — GC-free D with Mir

How to use Mir libraries in D's -betterC mode — no runtime, no GC, no exceptions. Covers mir-core, mir-algorithm, and mir-random in betterC contexts with practical examples.

D's -betterC flag disables the D runtime and garbage collector, making the resulting code directly linkable with C programs, embedded systems, and WebAssembly targets. Mir was designed with this use case in mind — its core libraries work fully in -betterC.

BetterC was one of the original motivations for the Mir project. The 2016 D forum thread "A betterC modular standard library" describes the design intent behind libmir.org.

What betterC disables

When you compile with -betterC (or "dflags": ["-betterC"] in dub.json):

  • No GC — new T is forbidden unless you manage memory manually
  • No D runtime — no TypeInfo, no module constructors
  • No exceptions — throw and try/catch are unavailable
  • No assert with messages — use assert(cond) only, no string arg

What still works: @safe, @nogc, nothrow, pure, templates, CTFE, static foreach, unions, structs, function pointers.

Setting up a betterC project

// dub.sdl
name "myproject"
targetType "staticLibrary"
dependency "mir-algorithm" version="~>3.0"
dependency "mir-core" version="~>1.0"
dflags "-betterC"

To detect BetterC at compile time within source code (does not enable it — use the compiler flag or dflags for that):

// betterc_module.d
module mylib.compute;

version (D_BetterC) {
    // Code in this block compiles only when -betterC is active
}

Using ndslice in betterC

slice!T(...) uses the GC — avoid it. Use rcslice (reference-counted) or wrap external buffers:

import mir.ndslice;
import mir.rc.array : rcslice;

// Option 1: Reference-counted (no GC)
auto m = rcslice!double(3, 4);
m[] = 0.0;

// Option 2: Wrap C-allocated memory
import core.stdc.stdlib : malloc, free;
auto ptr = cast(double*) malloc(12 * double.sizeof);
scope(exit) free(ptr);
auto m2 = ptr.sliced(3, 4);   // no ownership, no allocation

Algebraic types in betterC

mir.algebraic works fully without the runtime:

import mir.algebraic;

alias Result = Variant!(int, string);

extern(C) int compute(int x) @nogc nothrow
{
    Result r = x > 0 ? Result(x * 2) : Result("negative");
    return r.match!(
        (int v)    => v,
        (string s) => -1,
    );
}

Random numbers in betterC

import mir.random;
import mir.random.variable : NormalVariable;

extern(C) double sampleNormal() @nogc nothrow
{
    static Random rng;
    static bool init = false;
    if (!init) { rng = Random(42); init = true; }

    auto n = NormalVariable!double(0.0, 1.0);
    return n(rng);
}

Error handling without exceptions

Without exceptions, use mir.algebraic result types:

import mir.algebraic;

struct ParseError { string message; }
alias ParseResult = Variant!(double, ParseError);

ParseResult parseDouble(const(char)[] s) @nogc nothrow
{
    import mir.parse : fromString;
    double val;
    if (!s.fromString(val))
        return ParseResult(ParseError("invalid number"));
    return ParseResult(val);
}

// At the call site:
auto r = parseDouble("3.14");
r.match!(
    (double v)      => writeln("parsed: ", v),
    (ParseError e)  => writeln("error: ", e.message),
);

Interop with C

BetterC D compiles to standard object files that link directly with C:

// mylib.d — compiled with -betterC
import mir.ndslice;

extern(C) void normalize(double* data, size_t n) @nogc nothrow
{
    auto v = data.sliced(n);

    import mir.algorithm.iteration : reduce;
    import mir.math.common : sqrt;

    double sum2 = v.reduce!"a + b*b"(0.0);
    double norm = sqrt(sum2);
    if (norm > 0)
        v[] = v.map!(x => x / norm);
}
// main.c
extern void normalize(double* data, size_t n);

int main() {
    double data[] = {1.0, 2.0, 3.0};
    normalize(data, 3);
    // data is now [0.267, 0.535, 0.802]
}

Compile:

ldc2 -betterC -c mylib.d -of=mylib.o
cc main.c mylib.o -o app

What to avoid

PatternProblemAlternative
new T(...)GC allocationStack alloc, rcslice, or manual malloc
throw eExceptionsReturn Variant!(T, Error)
string literals in assertRuntimeassert(cond) only
foreach (k, v; aa)AA uses GCUse sorted arrays or hash maps from mir
std.conv.to!stringGCmir.format or mir.conv
writelnRuntimeprintf or mir.stdio

WebAssembly target

BetterC D compiles to Wasm via LDC:

ldc2 -betterC -mtriple=wasm32-unknown-unknown-wasm \
     -link-internally -of=mylib.wasm mylib.d

Mir's @nogc guarantee means zero GC pauses — critical for Wasm performance.

On this page