Thread overview
how much "real-life" code can be marked @safe ?
Jul 02, 2021
someone
Jul 02, 2021
Alexandru Ermicioi
Jul 02, 2021
tsbockman
Jul 03, 2021
Alexandru Ermicioi
Jul 03, 2021
tsbockman
Jul 04, 2021
Alexandru Ermicioi
Jul 04, 2021
Paul Backus
July 02, 2021

... just wondering:

I am writing pretty trivial code, nothing out of the ordinary, and attempted to check how much of it could be marked safe ...

  • Lots of tiny common library functions are pretty easy

  • Getter/Setter properties are easy too

  • almost all this() constructors are a no-go providing you do something in-between with the parameters until you assign them back to the target variables; eg: you have a char parameter (that you need to do something with it) that needs to be assigned to a class/structure string member variable and then it needs a cast ... but no castings are allowed with @safe

But when you start attempting to declare @safe chunks of code that actually DO things ... well, it seems end-of-the-story.

Declaring @safe void() main() {...} as I was advised in some previous post (to avoid declaring @safe everywhere) is almost impossible unless you are doing the hello world app.

I would love to hear how you, I mean the community, approach code safeness ?

July 02, 2021

On Friday, 2 July 2021 at 00:26:52 UTC, someone wrote:

>

... just wondering:
...

Imho, if you want all of the app to be safe, and you cannot avoid unsafe code, then there are two choices:

  1. Mark the method doing unsafe stuff as @trusted, or pieces of code which are unsafe with trusted lambda hack.
  2. Mark the top caller of your @system methods as @trusted.

In both cases, unsafe code should be manually checked as best as it can be done. Best is to think twice whether you can redesign your code to avoid unsafe operations.

On PR review step if there is such thing for your app, reviewers should carefully review those @trusted blocks of code.

Oh well, and tests, tests and more tests, i.e. It should be thoroughly tested.

Best regards,
Alexandru.

July 02, 2021

On 7/1/21 8:26 PM, someone wrote:

>

... just wondering:

I am writing pretty trivial code, nothing out of the ordinary, and attempted to check how much of it could be marked safe ...

It should be quite a bit.

>
  • Lots of tiny common library functions are pretty easy

  • Getter/Setter properties are easy too

  • almost all this() constructors are a no-go providing you do something in-between with the parameters until you assign them back to the target variables; eg: you have a char parameter (that you need to do something with it) that needs to be assigned to a class/structure string member variable and then it needs a cast ... but no castings are allowed with @safe

This doesn't make a lot of sense, lots of constructors are safe.

And casting is OK as long as it's safe, including class downcasts.

>

But when you start attempting to declare @safe chunks of code that actually DO things ... well, it seems end-of-the-story.

This is why you encapsulate the unsafe parts in @trusted functions.

The classic example is the posix read and write functions. The function int read(int fd, void *data, int len) is not safe, nor can it be trusted. But a trusted wrapper is possible: @trusted int readSafe(int fd, ubyte[] data). Now all of a sudden, safe functions have more utility.

>

Declaring @safe void() main() {...} as I was advised in some previous post (to avoid declaring @safe everywhere) is almost impossible unless you are doing the hello world app.

The point is to mark main @safe, and then fix whatever things that aren't safe that it calls to provide a safe interface.

>

I would love to hear how you, I mean the community, approach code safeness ?

Any function I want to make safe, I mark it as @safe. Then I fix the compiler complaints until it compiles (including functions that it calls), using as little @trusted code as possible.

-Steve

July 02, 2021

(Responding out of order:)

On Friday, 2 July 2021 at 00:26:52 UTC, someone wrote:

>

But when you start attempting to declare @safe chunks of code that actually DO things ... well, it seems end-of-the-story.

If you find yourself unable to get real work done in @safe code, this is almost certainly a sign of one of the following problems:

  1. You don't fully understand the purpose and valid use of any or all of the @trusted, inout, scope, and return annotations.

  2. Your code is avoiding use of the garbage collector, and/or does not have -dip1000 enabled. (@safe is still quite useful without the garbage collector, but even with -dip1000 you'll still need a lot of @trusted code.)

  3. You have at least one dependency that isn't correctly designed for use with @safe.

As long as you're willing to use the garbage collector, almost all algorithms can be expressed in an efficient @safe way, but this sometimes requires knowledge of several advanced features of D, and how and when to combine them.

>

but no castings are allowed with @safe

That is simply not true. Many explicit casts are @safe, as are nearly all implicit casts.

Casting away const or immutable is @system, but should hardly ever be necessary if you understand how to write constructors correctly (see below), and use inout appropriately.

Many reinterpret casts are also illegal, but union and class provide @safe ways of achieving the same goals for the common cases.

>
  • almost all this() constructors are a no-go providing you do something in-between with the parameters until you assign them back to the target variables;

Constructors can be @safe, but you have to understand how they work in D:

The key difficulty is that what appears to be the first "assignment" to each field in a constructor actually constructs that field, instead. Subsequent assignments really are assignments.

So, when constructing a const object each field can only be "assigned" once, because the fields are also const, even in the object's constructor. If you need to do complex calculations to determine field values, use temporary variables and static helper functions until you get the final value, and then unconditionally assign that value to a field once.

TLDR; You probably don't understand how to use @safe correctly. (Most people don't; the rules are complicated and non-obvious.)

Post some example code that you think can't be @safe, and I can probably show you how to fix it, unless it involves manual memory management or an incompatible dependency. Even then, the non-@safe code can often be isolated behind an @trusted API.

July 03, 2021

On Friday, 2 July 2021 at 22:08:31 UTC, tsbockman wrote:

>

(Responding out of order:)

On Friday, 2 July 2021 at 00:26:52 UTC, someone wrote:

>

But when you start attempting to declare @safe chunks of code that actually DO things ... well, it seems end-of-the-story.

If you find yourself unable to get real work done in @safe code, this is almost certainly a sign of one of the following problems:

  1. You don't fully understand the purpose and valid use of any or all of the @trusted, inout, scope, and return annotations.

  2. Your code is avoiding use of the garbage collector, and/or does not have -dip1000 enabled. (@safe is still quite useful without the garbage collector, but even with -dip1000 you'll still need a lot of @trusted code.)

  3. You have at least one dependency that isn't correctly designed for use with @safe.

I'd add:
3. An edge case. Ex: You need to mutate some data and then assume it is immutable in a constructor.

  1. Functionality that doesn't account for @safe/immutable or any other features when it can in standard library. Take for example array.dup, there is no inout alternative for it, and you're pretty much stuck with trusted code, when you'd like to dup an array that is inout.
    manual.

Although these two should be on the lowest place in this list by priority.

July 03, 2021

On Saturday, 3 July 2021 at 16:06:33 UTC, Alexandru Ermicioi wrote:

>
  1. An edge case. Ex: You need to mutate some data and then assume it is immutable in a constructor.

Can you give a valid example where that is necessary? The main examples that I can think of either can be @safe with the right API, or are motivated by a desire to avoid the GC and/or druntime, thus falling under (1).

>
  1. Functionality that doesn't account for @safe/immutable or any other features when it can in standard library.

True, although it's just another example of my point (2). The standard library and druntime are dependencies, too...

>

Take for example array.dup, there is no inout alternative for it,
and you're pretty much stuck with trusted code, when you'd like to dup an array that is inout.

inout is usually just a convenient way to use one implementation to handle mutable, const and immutable cases. In those rare cases where inout itself won't work, it is almost always possible to accomplish the same thing using template this or separate overloads:

import std.traits : Unqual, CopyConstness;

struct A {
    int*[] arr;
    this(this This, Arr)(Arr arr) scope pure @safe nothrow
        if(is(Arr : E[], E) && is(E : CopyConstness!(This, Unqual!E)))
    {
        this.arr = arr.dup;
    }
}

void main()
{
    A ma = new int*[5];
    const(A) ca = new const(int*[3]);
    const(A) ia = new immutable(int*[4]);
}

Again, it's certainly not obvious how to do this, or why it is necessary, but it is possible.

The one exception here is when the array is already typed inout before it is passed to the constructor. But, that's an example of (2) since this logic applies transitively throughout the call stack: if you need to call dup anywhere, don't erase the constness with inout.

July 04, 2021

On Saturday, 3 July 2021 at 20:09:56 UTC, tsbockman wrote:

>

On Saturday, 3 July 2021 at 16:06:33 UTC, Alexandru Ermicioi wrote:

>
  1. An edge case. Ex: You need to mutate some data and then assume it is immutable in a constructor.

Can you give a valid example where that is necessary? The main examples that I can think of either can be @safe with the right API, or are motivated by a desire to avoid the GC and/or druntime, thus falling under (1).

Can't remember any specific code now, but suppose you have a mutable object as input to a function or constructor. You need to return or assign an immutable copy of that struct and you can do that with right copy constructor on that object, but before that you need to do a couple of mutations on that object. In this use case you can't avoid cast(immutable) easily. Note: it is desired to not mutate the original object.

The summary is this: the construction of immutable instances can be done just through initialization statement. You can't mutate the instance and then assign/assume it as an immutable.

> >
  1. Functionality that doesn't account for @safe/immutable or any other features when it can in standard library.

True, although it's just another example of my point (2). The standard library and druntime are dependencies, too...

Right, didn't take it that way.

>

The one exception here is when the array is already typed inout before it is passed to the constructor. But, that's an example of (2) since this logic applies transitively throughout the call stack: if you need to call dup anywhere, don't erase the constness with inout.

Yeah this is a working workaround, for this specific use case. I still would prefer inout version, even if I need to do some unsafe casts due to clear intentions it gives the user, and better errors.
Anyway this is just a proof of your 2nd point.

July 04, 2021

On Sunday, 4 July 2021 at 08:43:11 UTC, Alexandru Ermicioi wrote:

>

On Saturday, 3 July 2021 at 20:09:56 UTC, tsbockman wrote:

>

On Saturday, 3 July 2021 at 16:06:33 UTC, Alexandru Ermicioi wrote:

>
  1. An edge case. Ex: You need to mutate some data and then assume it is immutable in a constructor.

Can you give a valid example where that is necessary? The main examples that I can think of either can be @safe with the right API, or are motivated by a desire to avoid the GC and/or druntime, thus falling under (1).

Can't remember any specific code now, but suppose you have a mutable object as input to a function or constructor. You need to return or assign an immutable copy of that struct and you can do that with right copy constructor on that object, but before that you need to do a couple of mutations on that object. In this use case you can't avoid cast(immutable) easily. Note: it is desired to not mutate the original object.

immutable(Foo) example(ref Foo input)
{
    Foo mutableCopy = input;
    mutableCopy.mutate();
    return immutable(Foo)(mutableCopy); // call copy ctor
}

I guess if your object is very expensive to copy you might want to use cast(immutable) here, but IMO the real solution is to refactor your code so that the call to .mutate() is not necessary.

For example, let's say that what mutate does is change the member variable bar. You could rewrite the above as:

immutable(Foo) example(ref Foo input)
{
    auto copy = immutable(Foo)(
        // update this field
        computeNewValue(input.bar),
        // copy the rest
        input.baz,
        input.quux,
        /* ... */
    );
    return copy;
}