@trusted considered harmful

David Nadlinger see at klickverbot.at
Fri Jul 27 17:08:28 PDT 2012


@trusted in its current form needs to go. Its design is badly 
broken, as it leaks implementation details and encourages writing 
unsafe code.

The Problem
———————————

First, there is no point in having @trusted in the function 
signature. Why? From the perspective of the caller of the 
function in question, @safe and @trusted mean exactly the same 
thing. If you are not convinced about that, just consider that 
you can wrap any @trusted function into a @safe function to make 
it @safe, and vice versa.

So the current situation is similar to having two keywords `pure` 
and `pure2` in the language, which are completely equivalent for 
the consumer of an API. This is in itself a problem, since it is 
a pitfall for writing generic code. To stick with the example, 
it's easy to check only for `pure` in a template constraint when 
you really meant to accept both `pure` and `pure2` – and you 
_always_ want to accept both.

But is this alone enough to warrant a change to the language at 
this point? Probably not. But besides that, the current design 
also leads to problems for the implementation side:

One issue is that the distinction unnecessarily restricts the 
implementation in terms of interface stability. Yes, @safe and 
@trusted are equivalent from the caller's perspective, but they 
are mangled differently. This means that changing a function 
signature from one to the other is a breaking change to the ABI, 
and as the mangled name is available in the program (which is 
e.g. what std.traits.FunctionAttributes), also to the API.

Thus, you can't just change @trusted to @safe or vice versa on 
the implementation side if you make changes code which require 
@trusted, resp. cause it to be no longer needed. Sure, you can 
always move the implementation into a new, properly marked 
function, and make the original function just a wrapper around 
it. But this is kludgy at best, and might be unacceptable in the 
case of @safe -> @trusted for performance optimizations, if the 
inliner doesn't kick in. So, the only reasonable choice if you 
want to provide a stable interface in this case is to mark all 
functions which could ever possibly need to access unsafe code as 
@trusted, forgoing all the benefits of automatic safety checking.

But the much bigger problem is that @trusted doesn't play well 
with template attribute inference and makes it much too easy to 
accidentally mark a function as safe to call if it really isn't. 
Both things are a consequence of the fact that it can be applied 
at the function level only; there is no way to apply it 
selectively to only a part of the function.

As an example how this is problematic, consider that you are 
writing a function which takes some generic input data, and needs 
to do (unsafe) low-level buffer handling internally to 
efficiently do its job. You come up with a first implementation, 
maybe only accepting arrays for the sake of getting it working 
quickly, and add @trusted as your dirty buffer magic isn't 
visible from the outside, but does break attribute inference. 
Later, you decide that there is no reason not to take other range 
types as input. Fortunately, the actual implementation doesn't 
require any changes, so you just modify the template constraint 
as needed, and you are good. Well, no – you've just completely 
broken all safety guarantees for every program which calls your 
function, because empty/front/popFront of the passed range might 
be @system.

Now, you might argue that this is a contrived scenario. Yes, the 
mistake could have easily be avoided, @trusted on a template 
declaration should always raise a red flag. But cases like this 
_do_ occur in real-world code, and are easy to miss: The recently 
added std.uuid originally had a similar bug, which went unnoticed 
until during the vote [1] – at that point, a number of people, 
mostly experienced contributors, had reviewed the code. A safety 
system which is easy to break by accident is somewhat of a futile 
exercise.

Can you correctly implement such a template function with today's 
@trusted? Yes, there are workarounds, but it's not quite easy. 
One way is to explicitly detect the @safe-ty of the code accessed 
via template arguments and switch function prototypes using 
static ifs and string mixins to avoid code duplication. For an 
example of this, see Jonathan's new std.range.RefRange [2]. It 
works, but it isn't pretty. The average programmer will, just as 
done in the revised version of std.uuid [3], likely give up and 
accept the fact that the function isn't callable from safe code. 
Which is a pity, as we should really utilize the unique asset we 
got in SafeD to the fullest, but this only works if everything 
that could be @safe is marked as such. The situation won't get 
better as we continue to advocate the use of ranges, either.

To summarize, there are, at least as far as I can see, no 
advantages in distinguishing between @safe and @trusted in 
function signatures, and the function-level granularity of 
@trusted yields to avoidable bugs in real-world code. 
Fortunately, we should be able to resolve both of these issues 
fairly easily, as described below.


A Solution
——————————

Let me make something clear first: I am _not_ intending to remove 
@trusted from the language. As a bridge between the @safe and 
@system worlds, it is an integral part of SafeD. What I'm 
proposing is:

  1) Remove the distinction between @safe and @trusted at the 
interface (ABI, API) level. This implies changing the name 
mangling of @trusted to Nf, and consequently removing the 
distinction in DMD altogether (at least in user-facing parts like 
.stringof and error messages). In theory, this is a breaking 
change, but as any code that doesn't treat them the same is buggy 
anyway, it shouldn't be in practice. As for 
std.traits.FunctionAttribute, we could either make trusted an 
alias for safe, or just remove documentation for the former and 
keep it around for some time (there is no way to deprecate an 
enum member).

  2) The first step is necessary, but mainly of cosmetic nature 
(think `pure`, `pure2`). We still need to address for the 
granularity and attribute inference problem. The obvious solution 
is to add a "@trusted" declaration/block, which would allow 
unsafe code in a certain region. Putting @trusted in the function 
header would still be allowed for backwards compatibility (but 
discouraged), and would have the same effect as marking the 
function @safe and wrapping its whole body in a @trusted block. 
It could e.g. look something like this (the @ prefix definitely 
looks weird, but I didn't want to introduce a new keyword):

---
  void foo(T)(T t) {
    t.doSomething();
    @trusted {
      // Do something dirty.
    }
    t.doSomethingElse();
    @trusted phobosFunctionWhichHasNotBeenMarkedSafeYet();
  }
---

This is similar to other constructs we have today, for example 
debug {}, which allows impure code. It can be debated whether a 
block »argument« should introduce a new scope or not (like 
static if). The latter currently seems much more attractive to 
me, but I suppose it could be confusing for some.

In any case, while there is probably quite a bit of bikeshedding 
to be done for 2), I don't think there is much controversy about 
1). So, let's try to get this done shortly after the 2.060 
release – as discussed above, it is very unlikely that the 
change will break something, but the odds still increase over 
time. Also, there is currently a Phobos pull request [4] which 
will be influenced by the outcome of this discussion.

David



[1] 
http://forum.dlang.org/thread/jrpni1$rt$1@digitalmars.com?page=2#post-pcaaoymspzelodvmnbvc:40forum.dlang.org
[2] 
https://github.com/D-Programming-Language/phobos/blob/c005f334ec34d3b0295d5dbf48212972e17823d0/std/range.d#L7327
[3] 
https://github.com/D-Programming-Language/phobos/blob/c005f334ec34d3b0295d5dbf48212972e17823d0/std/uuid.d#L1193
[4] https://github.com/D-Programming-Language/phobos/pull/675


More information about the Digitalmars-d mailing list