Skip to content

Best Practices

Timur Gafarov edited this page Feb 5, 2024 · 43 revisions

This page describes an idiomatic way of writing dlib-friendly code. You are strongly encouraged to read this before writing new modules for the library and extending existing ones.

Memory

Writing effective and predictable real-time applications require full control over dynamic memory allocation and freeing. Garbage collector, which is one of the most controversial features of D, is unaffordable for a strict real-time system. Luckily we can do allocations manually with malloc instead of D's native heap allocator, but isn't it a step backward into the old days of С? Not with dlib!

dlib allows you to handle memory management in two fashions: C++ style (using New and Delete) and Delphi style (using ownership).

New and Delete are template functions that wrap malloc and free. They allocate and free arrays, classes and structs, properly initializing them and calling constructors when necessary:

import dlib.core.memory;

void main()
{
    int[] array = New!(int[])(100);
    Delete(array);

    MyClass obj = New!MyClass(1, 2, 3);
    Delete(obj);
}

This may be enough for many simple cases, but if you're writing an application with lots of objects things can quickly become complicated. The memory may end up leaking if you forget to delete some object, and bugs like that are not the easiest to fix. Garbage collection is there to solve this problem, but it brings plenty of other issues, so dlib provides an alternative - object ownership.

Core concept of ownership is that any object may 'belong' to other object. When the owner object is deleted, all its belonging objects are also automatically deleted.

import dlib.core.memory;
import dlib.core.ownership;

class MyClass: Owner
{
    this(Owner owner = null)
    {
        super(owner);
    }
}

void main()
{
    MyClass c1 = New!MyClass();
    MyClass c2 = New!MyClass(c1);
    Delete(c1); // c2 is deleted automatically
}

This, of course, imposes some restrictions on object's lifetime, but the idea of ownership can be applied to many use cases, GUI and game engines being two common examples. If you can think of your data model as a tree, ownership is a perfect memory management policy for it.

If you want to release non-owned entities (like arrays) or external resources (such as library or VRAM data) allocated by your object, do it in object's destructor:

class MyClass: Owner
{
    //...

    ~this()
    {
        Delete(myArray);
        free(myExternalData);
    }
}

It is not recommended to use GC-allocated data in such objects.

Comparison

D's native memory management with GC

  • Pros: easiest to use, part of the language syntax.
  • Cons: unpredictable, requires additional logics to correctly handle external resources, still can cause memory leaks if used carelessly.
  • Summary: best suited for applications that constantly allocate short-living objects (such as servers), or for utilities that perform one task and then exit. Not suitable for game engines that work heavily with VRAM data, which cannot be managed with GC.

Pure New/Delete

  • Pros: predictable, gives full control over memory management.
  • Cons: requires too much discipline, unconvenient to use in big projects, often causes memory leaks and double free errors
  • Summary: best suited for relatively small apps and games with simple engine architecture. In large projects turns code into unmaintainable mess.

Ownership

  • Pros: predictable, requires little or no programmer attention.
  • Cons: constraints objects lifetime, works only with classes, can be an overkill for small applications.
  • Summary: best suited for complex games and GUI applications. Useless if you don't write in object-oriented style.

Memory profiling

dlib includes built-in memory profiler that helps you to debug memory leaks when using New and Delete. Usage is the following:

import dlib.core.memory;

void main()
{
    enableMemoryProfiler(true);
    // Run your code...
    printMemoryLeaks();
}

printMemoryLeaks will output a list of manually-allocated objects that were not deleted. Each entry is hinted with type name, size in bytes, and location (file and line) the object was allocated at.

Error Handling

Exceptions are common mean of error handling, but we recommend against using them. They tend to increase code complexity, they are hard to manage properly, and they make applications crash too often. In D they are also tied to OOP and garbage collector. We recommend a simpler alternative - Go-style error tuples. They can be easily constructed using Compound type:

import dlib.core.compound;

Compound!(Data, string) doSomething()
{
    Data data;

    // Process data

    if (someErrorOccured)
        return compound(data, "Error message");
    else
        return compound(data, "");
}

auto res = doSomething();
if (res[1].length)
    writeln(res[1]);
else
    // use res[0]

You can use nested functions to finalize your data before returning an error:

Compound!(Data, string) doSomething()
{
    Data data;

    Compound!(Data, string) error(string errorMsg)
    {
        // Finalize data...

        return compound(data, errorMsg);
    }

    // Process data...

    if (someErrorOccured)
        return error("Error message");
    else
        return compound(data, "");
}

Files

All file I/O in dlib is based on dlib.core.stream and dlib.filesystem. The latter supports both garbage-collected and manual memory management.

For simple cases use free functions from dlib.filesystem.local:

import dlib.core.stream;
import dlib.filesystem.local;

InputStream s = openForInput("my.file");
// can read from s

OutputStream s = openForOutput("my.file");
// can write to s

The code above will allocate GC memory. It's GC-free version is the following:

import dlib.filesystem.stdfs;

StdFileSystem fs = New!StdFileSystem();

InputStream s = fs.openForInput("my.file");
// can read from s
Delete(s);

OutputStream s = fs.openForOutput("my.file");
// can write to s
Delete(s);

Delete(fs);

Containers

dlib provides GC-free alternatives to D's native dynamic and associative arrays - dlib.container.array and dlib.container.dict, respectively.

import dlib.container.array;

Array!int a;
a ~= 10;
a ~= [11, 12, 13];
foreach(i, ref v; a)
    writeln(v);
a.removeBack(1);
assert(a.length == 2);
a.free();
import dlib.container.dict;

Dict!(int, string) d = New!(Dict!(int, string))();
d["key1"] = 10;
d["key2"] = 20;
Delete(d);

Strings

dlib has its own mutable string object which doesn't use GC. It uses UTF-8 and is compatible with native string type.

import dlib.text.str;

String s = "hello";
s ~= ", world";
s ~= '!';
string dStr = s;
assert(dStr == "hello, world!");
s.free();

It also uses SSO (short string optimization), so that strings of less than 128 bytes live entirely on the stack and don't allocate memory. Another advantage is that String is implicitly zero-terminated, so it can be passed to C libraries without onerous toStringz.

Clone this wiki locally