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

Steven Schveighoffer schveiguy at gmail.com
Sat Apr 4 16:44:48 UTC 2020


On 4/4/20 2:53 AM, Walter Bright wrote:
> On 4/3/2020 2:06 PM, Steven Schveighoffer wrote:
>> But what should absolutely not compile is:
>>
>> extern(C) int free(void *);
>>
>> void foo(int *ptr) // now inferred @safe
>> {
>>     free(ptr);
>> }
> 
> I understand your proposal. You want C functions without bodies to be 
> @system.

This is the bare minimum proposal. There are various options as I laid out.

My preference is that ALL extern(C), extern(C++), extern(Objective-c) 
functions are @system by default, with or without body. This is the 
least intrusive and most consistent proposal that does not gut all 
safety guarantees in D.

>> The fact that we cannot control where/how people define their 
>> prototypes means we have to be firm on this. They need to opt-in to 
>> @safe with extern(C), it cannot be default.
> 
> On the other hand, special cases like this tend to cause unexpected 
> problems in the future. Experience pretty much guarantees it. It's 
> likely to be tricky to implement as well.

This is like saying we shouldn't keep loaded guns in a locked safe 
because we don't keep our shirts in a locked safe, because it's too 
complicated and "tricky to implement". Yep, it's not as easy. That's 
because it's dangerous.

> People remember simple rules. They don't remember rules with odd 
> exceptions to them, that always winds up with trouble and bug reports. 
> Simple rules applied evenly lead to a compiler that works and is 
> reliable. I'm afraid the weight of all the special rules will crush D.

This is not a "special rule" or even a complicated one. It's really 
simple -- extern(C) functions cannot be trusted, so they need to be 
@system by default. H.S. Teoh put it perfectly:

> The rule:
> 
> 	extern(D) => @safe by default
> 	extern(C) => @system by default
> 
> hardly sounds "odd" to me.  It almost verbally describes what C is, and
> what we want D to be, there's nothing easier to remember.

Continuing...

> Now, as to what to do. I spent a few minutes and added `@system:` in to 
> the C headers in druntime for windows, posix, and linux. Done. I hope 
> someone else will do it for freebsd, etc., if not I'll go do that to.

I don't even have to look to know that you didn't get them all. There 
are peppered extern(C) prototypes all over Druntime and Phobos. I 
pointed out one in another post which you have not replied to. This does 
not even mention all D code in all repositories which frequently add an 
extern(C) prototype for functions needed. Our current documentation says 
[https://dlang.org/spec/interfaceToC.html#calling_c_functions]:

> C functions can be called directly from D. There is no need for wrapper functions, argument swizzling, and the C functions do not need to be put into a separate DLL.
> 
> The C function must be declared and given a calling convention, most likely the "C" calling convention, for example:
> 
> extern (C) int strcmp(const char* string1, const char* string2);
> and then it can be called within D code in the obvious way:
> import std.string;
> int myDfunction(char[] s)
> {
>     return strcmp(std.string.toStringz(s), "foo");
> }

Hey look, there's a safety violation right there! It doesn't say, 
"import core.stdc.string where I've helpfully already pre-marked your 
system functions for you" it says, just spit out a prototype (without a 
@system tag) and you are good to go.

Let's update that documentation, and then wait for the questions "why 
does it say to use @system?"

"Oh, that's because D decided to trust all C calls as perfectly memory 
safe, so it's on you to tell the compiler it's not safe. Make sure you 
do that."

WAT.

> 
>  > is going to cause huge issues.
> 
> I doubt that for the simple reason that @system by default has not 
> caused huge issues.

@system by default is fine! It doesn't violate safety because you have 
to opt-in to trusting code. @safe by default for code that CANNOT BE 
CHECKED is wrong. Just plain wrong, and breaks safety completely.

@safe by default for code that CAN be checked is fine, because the 
compiler will check it. If it's not safe, it won't compile. This is why 
it's ok to mark @safe by default D functions with implementation, and 
even extern(D) prototypes (since the name mangling takes into account 
safety).

> 
> The rule is simple:
> 
> "For a D module with a bunch of C declarations in it, start it with 
> `@system:`."

This is your "simpler" solution? "Don't use the default because it's 
completely wrong"

And that is not the only place C prototypes are declared. People who 
have a C function they need to call follow the spec, and add a prototype 
into their module that is full of D code. Putting @system: at the top 
isn't a good idea.

> 
> It's not a hard rule to check. It's one line. D's C interface has always 
> relied on the user to get right. Recall that C doesn't do any name 
> mangling at all:
> 
>    ----- mylib.di
>      extern (C) int addOne(int i);
> 
>    ----- mylib.c
>      double addOne(double d) { return d + 1; }
> 
> 
> That'll link fine and yet fail at runtime. Oops! Calling it @system will 
> not help at all. If the C implementation is bad, there's not a damn 
> thing D can do about it, with or without @system. It's always going to 
> be this way.

This is a red herring -- code that doesn't work is not code that I care 
about. ALL unmarked extern(C) prototypes in existence today THAT ARE 
CORRECTLY WRITTEN are @system!!! Making them "magically" switch to @safe 
is wrong, BUT THEY WILL STILL WORK. However D safety will be utterly 
gutted and destroyed. I can't say this enough.

Consider a scenario:

Before the switch:

Some library has code that does i/o. They use prototypes to interface 
with the system calls as that's what the spec calls for:

extern(C) size_t read(int fd, void* buf, size_t length);

size_t doRead(int fd, void[] arr)
{
    return read(int fd, &arr[0], arr.length);
}

Now, let's see what user code can do here:

void main()
{
    int[10] arr;
    doRead(0, arr[]);
}

This runs and builds, and is technically fine! But it's not @safe. Which 
is OK because the user didn't opt-in to safety. This ALSO compiles:

void main()
{
    int[][10] arr;
    doRead(0, arr[]);
}

This compiles and builds and is NOT fine. It's now reading POINTERS out 
of stdin. It's still not @safe, and the user did not declare it safe, so 
it's on him (maybe he knows what he's doing).

Now, let's move to the future where your new DIP is the default.

BOTH versions of user code COMPILE, and are treated as @safe!!! Why? 
simply because the read prototype is now considered @safe.

This SILENTLY still works, and the library function doRead now is 
incorrectly @safe. Perhaps the user knew what he was doing when he was 
reading pointers from stdin. Maybe it's OK for now, but the library 
DEFINITELY is wrong for other uses.

You see, the problem isn't that "someone didn't do it right", it's that 
the thing that was right before is now wrong. Instantly, code that was 
completely correct in terms of memory safety is now completely 
incorrect. And it still silently builds and runs exactly as it did before.

Changes like this should REQUIRE a deprecation and error period.

But the easier thing is to simply avoid marking things safe that aren't 
safe. With that one change, this whole DIP becomes a benefit rather than 
the great destructor of all safe code in existence.

Please reconsider your position. This is so important, I think a virtual 
live discussion is in order if you are not convinced by this. Let me 
know, I'm working from home for 3 weeks already, I'm fine having one any 
time.

-Steve


More information about the Digitalmars-d mailing list