Skip to content

Instantly share code, notes, and snippets.

@aG0aep6G
Created June 29, 2019 21:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aG0aep6G/f267e316d1dce2763252d82c1808d53a to your computer and use it in GitHub Desktop.
Save aG0aep6G/f267e316d1dce2763252d82c1808d53a to your computer and use it in GitHub Desktop.

What's Safety?

In the D spec, and in this document, safety refers to an absence of undefined behavior. safe language features and safe functions cannot lead to undefined behavior as long they're combined with other safe features. It's implied in the definition that safety also means memory safety. Which means no memory corruption.

In contrast, unsafe features and functions are those that can lead to undefined behavior if used incorrectly. In particular, they might corrupt memory.

For example, dereferencing a pointer is considered safe. In actuality, dereferencing a pointer can have undefined behavior, if the pointer is invalid. And writing through such a pointer can certainly corrupt memory. In contrast, creating an invalid pointer doesn't have undefined behavior. But dereferencing is the more common and useful operation, so it has been decided that dereferencing is safe while features that might create invalid pointers are unsafe.

The Safety Attributes

D has three function attributes that deal with safety: @system, @safe, and @trusted.

@system is the default and provides no restrictions or guarantees. In @system code, it is your job as a programmer to avoid undefined behavior.

But history has shown that human programmers can't be trusted with that task, at least not on a large scale. That's why D has @safe. When you tag a function with @safe, the compiler will verify that it is in fact safe. If you try to use unsafe features, the compiler will reject your code. That means whatever bugs you might have in @safe code, they can never be bad enough to trigger undefined behavior; they will never lead to memory corruption.

The third attribute, @trusted is a sort of compromise between @system and @safe. Just like in @system functions, you can use unsafe features when implementing an @trusted function. The compiler doesn't put on its safety goggles when looking at your code. But like @safe functions, @trusted functions must be safe. They are not allowed to exhibit undefined behavior, and they're not allowed to affect the state of the program in an unsafe way.

For example, you can create an invalid pointer in an @trusted function. But you must not dereference it, and you must not return it from the function.

We know that writing @system code is hard (which is why we got @safe). Writing @trusted code is even harder. Not only do you have to avoid undefined behavior, you also have to ensure that the function as a whole is safe.

The best way to avoid mistakes in @trusted code is to not write @trusted code. If you think you have to do it, make sure you at least avoid these common pitfalls:

@trusted Pitfall 1: trustedFoo

Say you have some code that's mostly safe. But you need to call that one unsafe function foo. You know that you need to use @trusted, and the simplest way seems to write a little @trusted wrapper around foo. But don't just do this:

auto trustedFoo(M maybe, S some, P parameters)
    @trusted /* No! Bad programmer! */
{
    return foo(maybe, some, parameters);
}

If foo is unsafe, then so is trustedFoo. But @trusted functions must be safe.

It's unfortunate that we can find examples of this even in Phobos, D's standard library:

static void trustedFree(T[] p) @trusted
{
    import core.stdc.stdlib : free;
    free(p.ptr);
}
trustedFree(xform1);

(https://github.com/dlang/phobos/blob/b23950637d30906c7b1ca3ee31f4827e7af159c7/std/algorithm/sorting.d#L2982-L2987)

As long as the code stays exactly like this, it likely won't cause any trouble (I haven't actually checked thorougly). But the @safe attribute has already been undermined. In the future, some unlucky contributor might accidentally add another trustedFree(xform1) call. And then we're looking at a double free, which means undefined behavior.

Remember this sentence from above: "whatever bugs you might have in @safe code, they can never be bad enough to trigger undefined behavior; they will never lead to memory corruption". trustedFree breaks that guarantee.

@trusted Pitfall 2: Templates

Say you have a function template bar. You're using some unsafe features in the implementation, but you've made sure that they're used correctly. So you can mark the function as @trusted, right? No, usually you can't.

Most templates have completely arbitrary or only loosely restricted parameters. And D types can overload many operations that look benign at a cursory glance. Your code might end up making all sorts of function calls that didn't even look like calls to you.

For example, consider this simple function template:

auto bar(T)(T x)
{
    auto y = T(0);
    y = x;
    return x;
}

Looks like it doesn't do much at all, right? But it might call:

  • a constructor,
  • a destructor,
  • a copy constructor,
  • opAssign.

And all of those might be unsafe, which means they must not be called from @trusted code without special consideration. But you can't give special consideration when you don't even know what type T actually is.

Some other features that might lead to unexpected function calls are:

  • overloaded cast,
  • overloaded unary and binary operators like ! and +,
  • alias this.

The list is not meant to be complete.

@trusted Pitfall 3: Tiny Functions

An often-given advice for @trusted functions is to keep them small. The idea is to mark only those things as @trusted that actually need it. Then the rest of the code can enjoy the helpfully strict checks of @safe.

Generally, that's good advice, but you have to be careful. No matter how small it is, any @trusted function must be safe. Even if it's a function literal that is called immediately, the function must be safe.

Another bad example from Phobos:

b = (() @trusted => b.ptr[0 .. desired])();

(https://github.com/dlang/phobos/blob/0032c8d580b00de915a61df415689a09ce114eb9/std/experimental/allocator/building_blocks/bucketizer.d#L118-L120)

That tiny function isn't safe. Depending on the value of desired, it might make an out-of-bounds access, i.e. exhibit undefined behavior. And desired comes from @safe code. So a mistake in @safe code (setting desired incorrectly) can lead to undefined behavior. Safety has been undermined once again by an incorrectly applied @trusted.

Final Advice

I have showed some common mistakes in @trusted code, but I haven't exactly told you how to do it right. That's because I don't have a nice set of guidelines. I can often point out errors related to @trusted, but that doesn't mean that I could write it correctly without making some other mistake of my own.

By its nature, @trusted is a feature for experts. You need to know D in and out in order to use it correctly. You might need to know the language better than it is currently specified. And it's still a moving target, too, with a good amount of related compiler bugs.

This line from above might be the only true advice I can give: The best way to avoid mistakes in @trusted code is to not write @trusted code.

@carun
Copy link

carun commented Jan 6, 2020

An article from the official D blog from a while ago, albeit I'm not sure how this plays with the recent default @safe proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment