Discussion Thread: DIP 1028--Make @safe the Default--Final Review

Steven Schveighoffer schveiguy at gmail.com
Wed Apr 8 13:09:33 UTC 2020


On 4/7/20 10:01 PM, Timon Gehr wrote:
> On 07.04.20 23:14, Steven Schveighoffer wrote:
>> On 4/6/20 2:28 PM, Timon Gehr wrote:
>>> On 06.04.20 01:38, Steven Schveighoffer wrote:
>>>> I disagree with disallowing @safe as a specific marking on extern(C) 
>>>> code. You can write @safe extern(C) functions in D, and it makes no 
>>>> sense to require that they are @trusted at the prototype.
>>>
>>> The linker can hijack them. The function signature of a @trusted 
>>> function should be @safe anyway.
>>
>> The linker can always hijack it.
> 
> I guess I used the wrong word. What I meant was that there is zero 
> checking that the extern(C) function you are calling was actually 
> checked to be @safe. For extern(D), the language at least makes a small 
> effort to ensure the @safe qualifier has some meaning, but given that 
> return types are not mangled, that also seems like a lie.

If you change the return type, then there can be detectable differences 
at runtime. If you change the @safe vs. @trusted vs. @system, everything 
works *exactly* as before (unless the linker stops it).

The biggest problem is code that works but is wrong in terms of safety. 
In that case, @safe and @trusted on prototypes are identical.

> 
>> Even without intention. Having it marked @trusted isn't any better 
>> than having it marked @safe in that case.
>> ...
> 
> Memory corruption in a @safe context should be traceable to @trusted 
> code. It's the entire point.

And in this case, there is no trusted code. It's all @safe, but you have 
a boy-who-cried-wolf effect on the @trusted prototype. "Oh, you can 
ignore those prototypes because the compiler made me do it." In fact I'm 
assuming you will see stuff like:

// really @safe
@trusted extern(C) ...

causing a reviewer to pass over those as possible problems.

I'm not saying @safe marking of prototypes isn't prone to issues, I'm 
saying forcing @trusted markings doesn't change that fact.

> 
>>>
>>>> Assuming @safe, no. Explicitly @safe OK, you marked it, you own it.
>>>> ...
>>>
>>> @safe:
>>>
>>> // a lot of code
>>> // ...
>>>
>>> extern(C) corrupt_all_the_memory();
>>
>> Why would you do this when @safe is the default?
>> ...
> 
> To show it's broken.
> 
> Besides that, it's not currently the default, and if it is, you might 
> want to temporarily switch to @system and back.

In that case, all is well! you properly marked the right ones @system.

In the current regime, @safe: at the top does the same thing you are 
trying to argue against. So there won't be existing (good) code that 
does this.

In the new regime, @safe is the default, so you wouldn't need to put 
@safe: at the top.

Any time the compiler forces you to mark things differently than the 
truth, you become more numb to these compiler warnings, and don't put 
any stock into their significance.

> In any case, @safe code 
> by definition is code written by untrusted programmers. Why do you 
> insist there should be a good reason? Maybe the programmer was a monkey. 
> Or malicious.

@safe code is mechanically checked. Why shouldn't I be able to declare 
that when it's true? And if you make me mark it @trusted, and it for 
some reason becomes @system (because it's not @trustable, a very 
unlikely occurrence), having them marked @trusted doesn't help. You 
still have to go find the prototypes (all of them) and mark them @system 
instead.

> 
>>>
>>> When did @safe become a matter of "it's your own fault if you shoot 
>>> yourself in the foot, this memory corruption you are having is a good 
>>> thing because it will teach you not to make more mistakes in the 
>>> future"? If something may break @safe-ty as it trusts the programmer 
>>> to get it right, it ought to be @trusted.
>>
>> If you mark a @safe extern(C) function @trusted, how does that help? 
>> I'm talking about extern(C) functions that are checked by the compiler 
>> as @safe. Why should I have to mark the prototype of that function 
>> @trusted?
> 
> Because the extern(C) function can be changed to be @system. Hence you 
> must trust the maintainer of the prototype to keep it in sync with the 
> implementation.

So you are saying this scenario is OK:

Hm... my @safe function is turning into @system. But that's OK because 
everyone had to mark their prototypes @trusted! So now it becomes their 
fault I changed it.

I don't get how this is helpful. I don't get how this is somehow 
logically superior to the same people being able to mark the functions 
@safe. In both cases, you are pulling the rug from underneath them.

It sounds more like a "good" non-monkey programmer should mark all 
extern(C) function prototypes @system, regardless of the actual safety 
of the function, and require @trusted escapes, because they can change 
at any time without warning and they might get blamed. This is what I 
meant by a bureaucratic solution.

>> How does that prevent problems?
> 
> The point is to be able to trace any problems to some @trusted 
> annotations. @safe alone doesn't completely prevent memory safety 
> problems, but it advertises giving you a way to completely avoid being 
> blamed for them. (Except for your choice of programming language, I 
> suppose.)

In my interpretation, @safe means the code inside the function was 
mechanically checked. This doesn't change when you are writing 
prototypes for your @safe functions.

> 
>> It's functionally equivalent.
> 
> $ grep @trusted *.d

And get a large noise/signal ratio of frivolous @trusted markings for 
extern(C) functions that are really @safe but were forced by the 
compiler to mark them @trusted. How does this help find the problem?

> 
> Also, if you had in fact successfully identified a case where @safe and 
> @trusted are functionally equivalent, why would that not ring alarm 
> bells? If you write @safe, you don't actually mean @trusted.

Functionally equivalent in that you can interchange them and it doesn't 
change anything in terms of ability to call or compile the functions 
(marking the prototypes, that is).

But to a reviewer, they are not functionally equivalent. @safe says, the 
function was compiled as a @safe function, @trusted says it was compiled 
as a @trusted function. At this level the difference between the two is 
informational, not functional. That's why they are functionally 
equivalent. I think we should allow the distinction on the prototype 
because we allow it on the implementation.

And technically, if you don't trust the prototype writer to get the 
safety right, you should verify he got the parameters and return type 
right as well. So really it should be:

grep extern(C) *.d

> 
>> It's not something that changes when the original extern(C) function 
>> for some reason becomes @system (unlike extern(D) code).
>>
>> I agree with the concept that it's impossible for the compiler to know 
>> that an extern(C) prototype is @safe, but I think it's more 
>> bureaucratic than effective to make you use @trusted instead of @safe. 
>> It's like a law that has roots in good policy, but results in a lot of 
>> frivolous enforcement.
>> ...
> 
> I really don't understand why anyone would argue in favor of a policy 
> that allows code to become less safe when you annotate it @safe.

I don't understand why someone arguing that actual @safe functions 
cannot be marked @safe because they later could become @system would 
argue that as long as the prototypes are marked @trusted that's OK to 
switch to @system because it's someone else's fault. In neither case is 
the function author blameless because others were stupid to trust him 
with their prototypes.

But with the ability to mark things @safe, you have the ability to tell 
the truth about the actual implementation.

What if the @safe prototype is auto-generated? Then whenever it changes 
to @system, the prototype will be changed. This means, you have a memory 
issue, you search for @trusted, find all these prototypes that are 
actually prototypes for @safe functions auto generated. You now get into 
the habit of ignoring all @trusted prototypes as possible issues because 
it's just noise. Better to focus on the @trusted implementations, which 
is where the real problem can happen.

> 
>>>> We have similar problems with inout -- preventing obvious incorrect 
>>>> cases makes sense, until it doesn't.
>>>
>>> This is not analogous. Here, the problem is that the "obvious 
>>> incorrect cases" where not actually incorrect.
>>
>> No, they were obvious.
> 
> They were not incorrect, and the bad reasoning about those cases was 
> caused by a lack of understanding of formal logic and type theory. This 
> is also the underlying cause for the inout-related type system holes.

I'll defer to you on type system stuff, and admit that my arguments for 
inout not working in the cases that cause problems were flawed. But they 
made obvious sense to me. I see the same philosophy in my bad 
consideration of cases for inout in your arguments for this @trusted 
requirement. Which is why I brought up the analogy.

-Steve


More information about the Digitalmars-d mailing list