@trust is an encapsulation method, not an escape
Steven Schveighoffer via Digitalmars-d
digitalmars-d at puremagic.com
Fri Feb 6 05:28:59 PST 2015
First, I want to say that I didn't want to cause a huge rift between D
developers with this, I didn't think this was such a drastic issue, just
a possible idea. But here we are. I hope we can mend this, and move
forward. But on to the discussion.
On 2/5/15 6:39 PM, Walter Bright wrote:
> Consider the following code excerpted from std.array.join:
>
> static U trustedCast(U, V)(V v) @trusted { return cast(U) v; }
> return trustedCast!RetType(result);
>
> This is because the compiler would complain that the following line
> would not be @safe:
>
> return cast(RetType)(result);
>
> The rationale is "I know it's safe, so I'll create an @trusted wrapper
> to eliminate the error." What comes next is "that's cumbersome. How
> about a better syntax:"
>
> trusted {
> return cast(RetType)(result);
> }
No. This was NEVER THE ARGUMENT.
Now, let me explain why the latter is BETTER.
It's better because I know where it is used. It's used in one place, and
I can squash it right there saying "No, you can't do this in this one
place." Instead of reviewing an API in ALL POSSBILE CONTEXTS (which if
trustedCast is a public API, would be a lot), I have to review one call
in ONE CONTEXT.
The former is WORSE because it can be used in 100 places. Now I have to
go through and fix ALL THOSE FUNCTIONS that use it, because its
interface was exposed to the whole of phobos.
The problem, as we have said many times, is maintenance. Sure, in both
cases they never should have shown up in the first place. But there they
are, and we now have to fix them. All of them.
And let's also talk about long term maintenance. Any time a @trusted
function is amended or tweaked, we have to reconsider all the contexts.
If you mark a single line as @trusted, then additional lines to the same
function would not need as much scrutiny, especially if you warn when
@trusted tainted data is touched.
> The trouble with it is, what if the cast is converting from an integer
> to a pointer? That could lead to memory corruption. The code allows a
> potentially memory corrupting operation to be inserted into code that is
> otherwise @safe.
And so, you reject that code in both cases, except in the case where you
don't define a callable API, you don't have to worry about any other
code or contexts. This has to be the worst example to explain your point.
> The only way to deal with it is to then manually review everything about
> 'RetType' and 'result' to prove to oneself that it is not converting
> random bit patterns into pointers. In other words, one is manually
> reviewing @safe code for memory corruption errors.
@trusted code is not @safe code. You are reviewing that one line in its
current context, not the whole function to see where it is called in
various contexts.
> The solution is to regard @trusted as a means of encapsulating unsafe
> operations, not escaping them. Encapsulating them means that the
> interface from the @trusted code is such that it is usable from safe
> code without having to manually review the safe code for memory safety.
> For example (also from std.array):
>
> static void trustedMemcopy(T[] dest, T[] src) @trusted
> {
> assert(src.length == dest.length);
> memcpy(dest.ptr, src.ptr, src.length * T.sizeof);
> }
>
> I don't have to review callers of trustedMemory() because it
> encapsulates an unsafe operation (memcpy) with a safe interface.
Sure. But let's consider it's called in one place:
trustedMemcopy(array[pos..pos+to_insert], tmp);
What if that became:
assert(tmp.length == to_insert); // same as src.length == dest.length
@trusted memcpy(array.ptr + pos, tmp.ptr, tmp.length);
What is missing here is, make sure the type of array and tmp is the
same. All one has to do is review the function to see that. You could
put in a static assert if you wish:
static assert(is(typeof(array) == typeof(tmp)));
If there are multiple places that it's called from, sure we can
encapsulate that in a function:
static void trustedMemcopy(T[] dest, T[] src)
{
assert(src.length == dest.length);
@trusted memcpy(dest.ptr, src.ptr, src.length * T.sizeof);
}
There is very little difference here. Except if I add code to my version
of trustedMemcopy that is @system, I have to properly mark that too, and
that should imply greater scrutiny. I fail to see why making extra
unintended @system calls inside a "trusted" function shouldn't require
extra marking. Consider if this is a github review, and the context for
the new lines is missing (i.e. you don't see that the new line is inside
a @trusted function because github doesn't give you that line).
> The reason @trusted applies only to functions, and not to blocks of
> code, is that functions are the construct in D that provides an
> interface. Arbitrary blocks of code do not have a structured interface.
> Adding @trusted { code } support will encourage incorrect uses like the
> opening example. The existence of @trusted blocks will require review of
> every line of code in the function that encloses it, and transitively
> every function that calls it!
This is like opposite land! You don't have to review every function that
calls a @safe function with @trusted escapes any more than you have to
review every function that calls a @trusted function! That is the point
of having the attribute on the function call.
However, if @trusted is improperly placed on a function, then I have to
break every function that calls it if it now becomes @system. While the
same would be for a @safe function that has incorrectly escaped @trusted
calls, the temptation to make those small calls into a nice neat public
API without context is not there.
Having @trusted as a function attribute *encourages* bad encapsulation
of unsafe calls WITHOUT CONTEXT. This is so easy to do with templates.
> Adding @trusted as a function attribute, on the other hand, only
> requires review of the function's interface to determine if it is
> acceptable to use in safe code. Safety review of its callers is
> unnecessary.
I see zero difference between a @trusted function and a @safe function
with @trusted escapes. You have to review both with the same scrutiny.
Except less so for the one with escapes because you only have to
scrutinize the @trusted calls.
You know, I'm surprised you have rejected H.S. Teoh's proposal because
it seems like you are stuck on the concept that nobody should have to
review @safe code for memory problems. Perhaps marking functions
@trusted and then having escapes with @system is a better way of looking
at it, because you do have to review that code for memory problems.
The bottom line of my reasoning is that code changes over time, by
different people. Context is forgotten. It's much better to have the
compiler verify you know what you are doing when working with @trusted
than it is to just allow anyone to inject code anywhere.
-Steve
More information about the Digitalmars-d
mailing list