www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.ideas - Prevent struct going into heap memory

reply Richard (Rikki) Andrew Cattermole <richard cattermole.co.nz> writes:
In response to some concerns around the situation for RAII 
structs going into closures, I am proposing a resolution to this 
that will be a language-wide guarantee, not an ultra-specific, 
not-a-guarantee solution.

https://github.com/dlang/dmd/issues/18704

If a struct destructor is annotated with an attribute 
`` stackonly`` it may only be called if the ``this`` pointer is 
allocated on the stack. It does not overload.

```d
struct RAII {
     ~this()  stackonly {}
}

void foo() {
     RAII* gc = new RAII; // Error: RAII can only be cleaned up if 
it is on the stack
     scope RAII* stack1 = new RAII; // ok
     RAII stack2 = RAII(); // ok
}
```

The attribute is inferred based upon the fields.

```d
struct Wrapper {
     RAII raii;

     ~this() /*  stackonly */ {}
}
```

As closure creation can see this, no variable that is stack only, 
cannot be moved into a closure.

Currently D does not model GC vs non-GC pointers, therefore 
moving into pointers is also disallowed.

```d
RAII* ptr = ...;
*ptr = RAII(); // Error
```

But only in `` safe`` code. For `` system`` code, it is allowed 
to by-pass this restriction for data structures.
Jul 20
next sibling parent monkyyy <crazymonkyyy gmail.com> writes:
On Sunday, 20 July 2025 at 17:04:11 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 In response to some concerns around the situation for RAII 
 structs going into closures, I am proposing a resolution to 
 this that will be a language-wide guarantee, not an 
 ultra-specific, not-a-guarantee solution.

 https://github.com/dlang/dmd/issues/18704

 If a struct destructor is annotated with an attribute 
 `` stackonly`` it may only be called if the ``this`` pointer is 
 allocated on the stack. It does not overload.

 ```d
 struct RAII {
     ~this()  stackonly {}
 }

 void foo() {
     RAII* gc = new RAII; // Error: RAII can only be cleaned up 
 if it is on the stack
     scope RAII* stack1 = new RAII; // ok
     RAII stack2 = RAII(); // ok
 }
 ```

 The attribute is inferred based upon the fields.

 ```d
 struct Wrapper {
     RAII raii;

     ~this() /*  stackonly */ {}
 }
 ```

 As closure creation can see this, no variable that is stack 
 only, cannot be moved into a closure.

 Currently D does not model GC vs non-GC pointers, therefore 
 moving into pointers is also disallowed.

 ```d
 RAII* ptr = ...;
 *ptr = RAII(); // Error
 ```

 But only in `` safe`` code. For `` system`` code, it is allowed 
 to by-pass this restriction for data structures.
wouldnt something in core like `bool onStack(void*)` be be simpler? ```d import core.???; void main(){ int i; void* j=cast(void*)&i; assert(onStack(j)); class bleh{} ... assert( ! onStack(&blehwhatever)); } ```
Jul 20
prev sibling next sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Sunday, 20 July 2025 at 17:04:11 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 In response to some concerns around the situation for RAII 
 structs going into closures, I am proposing a resolution to 
 this that will be a language-wide guarantee, not an 
 ultra-specific, not-a-guarantee solution.

 https://github.com/dlang/dmd/issues/18704

 If a struct destructor is annotated with an attribute 
 `` stackonly`` it may only be called if the ``this`` pointer is 
 allocated on the stack. It does not overload.
Isn't the obvious solution to this bug (and related ones like call for structs that are allocated in heap closures? Why introduce this weird, special-purpose hack instead of fixing the root cause of the problem? [1]: https://github.com/dlang/dmd/issues/19119
Jul 20
parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 21/07/2025 12:00 PM, Paul Backus wrote:
 On Sunday, 20 July 2025 at 17:04:11 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 In response to some concerns around the situation for RAII structs 
 going into closures, I am proposing a resolution to this that will be 
 a language-wide guarantee, not an ultra-specific, not-a-guarantee 
 solution.

 https://github.com/dlang/dmd/issues/18704

 If a struct destructor is annotated with an attribute `` stackonly`` 
 it may only be called if the ``this`` pointer is allocated on the 
 stack. It does not overload.
Isn't the obvious solution to this bug (and related ones like [issue that are allocated in heap closures? Why introduce this weird, special- purpose hack instead of fixing the root cause of the problem? [1]: https://github.com/dlang/dmd/issues/19119
Ah see, I am arguing that the GC should handle cleanup. Problem is, some people believe it shouldn't. Hence this proposal to give those that want that "reliability" control to make it so.
Jul 20
parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 21 July 2025 at 00:31:10 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 Ah see, I am arguing that the GC should handle cleanup.
Having read further, it looks like the problem is that the GC currently does not have access to the necessary information to call a closure's destructors at runtime (because D's closures are type-erased). It seems like it ought to be possible to add this information to the closure's context, though, at least in principle.
 Problem is, some people believe it shouldn't. Hence this 
 proposal to give those that want that "reliability" control to 
 make it so.
If there are people who believe that (a) the struct instance should be allocated with the GC, but (b) the GC should not be responsible for its cleanup, then those people are wrong, plain and simple. On the other hand, if people want to assume control over both the allocation and the cleanup, that's already possible with existing language features.
Jul 20
next sibling parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this proposal to
     give those that want that "reliability" control to make it so.
 
 If there are people who believe that (a) the struct instance should be 
 allocated with the GC, but (b) the GC should not be responsible for its 
 cleanup, then those people are wrong, plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup. So they want to ban destructors in closures. My position ends up being: you want predictability? Ok, lets solve for that. But the default should align with convenience. Just disabling destructors doesn't give the predictability that you seek, there has to be more language checks than that.
Jul 20
next sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 21 July 2025 at 02:10:09 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this 
 proposal to
     give those that want that "reliability" control to make it 
 so.
 
 If there are people who believe that (a) the struct instance 
 should be allocated with the GC, but (b) the GC should not be 
 responsible for its cleanup, then those people are wrong, 
 plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup. So they want to ban destructors in closures.
This seems like a totally self-inflicted problem. If they don't want to rely on the GC for cleanup, nobody is forcing them to. D has plenty of mechanisms for predictable lifetime management already.
 Just disabling destructors doesn't give the predictability that 
 you seek, there has to be more language checks than that.
What "checks" could we possibly introduce that would help with this?
Jul 20
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 21/07/2025 2:47 PM, Paul Backus wrote:
 On Monday, 21 July 2025 at 02:10:09 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this proposal to
     give those that want that "reliability" control to make it so.

 If there are people who believe that (a) the struct instance should 
 be allocated with the GC, but (b) the GC should not be responsible 
 for its cleanup, then those people are wrong, plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup. So they want to ban destructors in closures.
This seems like a totally self-inflicted problem. If they don't want to rely on the GC for cleanup, nobody is forcing them to. D has plenty of mechanisms for predictable lifetime management already.
The problem is closure creation isn't always predictable just from looking at your code. Add to it, not having localnogc and we don't have any guarantees to prevent this. Yes I'm acknowledging that these are either bugs or perceived bugs.
 Just disabling destructors doesn't give the predictability that you 
 seek, there has to be more language checks than that.
What "checks" could we possibly introduce that would help with this?
See my original post. Its because of transitory nature of structs with interaction with no guaranteed cleanup of pointers. ```d struct Wrapper { RAII raii; } void func() safe { Wrapper* wrapper = new Wrapper; // Error: Wrapper's destructor is stackonly due to field raii. } ``` If you want guaranteed cleanup the only way to make that happen is to get the struct on the stack where the compiler can see it. If you don't do this, you do not have the guarantee to begin with.
Jul 21
prev sibling parent reply Atila Neves <atila.neves gmail.com> writes:
On Monday, 21 July 2025 at 02:10:09 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this 
 proposal to
     give those that want that "reliability" control to make it 
 so.
 
 If there are people who believe that (a) the struct instance 
 should be allocated with the GC, but (b) the GC should not be 
 responsible for its cleanup, then those people are wrong, 
 plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup.
"Doctor, doctor, it hurts when I do this". The GC isn't even guaranteed to run, I don't know what the alternative would be.
Jul 31
parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 31/07/2025 8:42 PM, Atila Neves wrote:
 On Monday, 21 July 2025 at 02:10:09 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this proposal to
     give those that want that "reliability" control to make it so.

 If there are people who believe that (a) the struct instance should 
 be allocated with the GC, but (b) the GC should not be responsible 
 for its cleanup, then those people are wrong, plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup.
"Doctor, doctor, it hurts when I do this". The GC isn't even guaranteed to run, I don't know what the alternative would be.
The stack. That is the point, one of the positions on this is to limit such structs from ever leaving the stack, and the point of this proposal is to allow those who want such guarantees to have it.
Jul 31
parent reply Atila Neves <atila.neves gmail.com> writes:
On Thursday, 31 July 2025 at 21:32:39 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 On 31/07/2025 8:42 PM, Atila Neves wrote:
 On Monday, 21 July 2025 at 02:10:09 UTC, Richard (Rikki) 
 Andrew Cattermole wrote:
 On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this 
 proposal to
     give those that want that "reliability" control to make 
 it so.

 If there are people who believe that (a) the struct instance 
 should be allocated with the GC, but (b) the GC should not 
 be responsible for its cleanup, then those people are wrong, 
 plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup.
"Doctor, doctor, it hurts when I do this". The GC isn't even guaranteed to run, I don't know what the alternative would be.
The stack. That is the point, one of the positions on this is to limit such structs from ever leaving the stack, and the point of this proposal is to allow those who want such guarantees to have it.
What if Alice writes a library with a struct with a destructor and Bob wants to use said library and doesn't care if the destructor ever runs and is ok with GC allocating those structs?
Sep 03
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 03/09/2025 7:31 PM, Atila Neves wrote:
 On Thursday, 31 July 2025 at 21:32:39 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 On 31/07/2025 8:42 PM, Atila Neves wrote:
 On Monday, 21 July 2025 at 02:10:09 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 On 21/07/2025 2:03 PM, Paul Backus wrote:
     Problem is, some people believe it shouldn't. Hence this 
 proposal to
     give those that want that "reliability" control to make it so.

 If there are people who believe that (a) the struct instance should 
 be allocated with the GC, but (b) the GC should not be responsible 
 for its cleanup, then those people are wrong, plain and simple.
Ah no, they don't like the GC introducing unreliability to the cleanup.
"Doctor, doctor, it hurts when I do this". The GC isn't even guaranteed to run, I don't know what the alternative would be.
The stack. That is the point, one of the positions on this is to limit such structs from ever leaving the stack, and the point of this proposal is to allow those who want such guarantees to have it.
What if Alice writes a library with a struct with a destructor and Bob wants to use said library and doesn't care if the destructor ever runs and is ok with GC allocating those structs?
Bob probably decided not to use Alice's library before this happened. If you go out of your way to import core.attributes, apply attributes in your code, and then build everything around that, you will be making decisions that will never suit somebody who doesn't care about any of that stuff. If it really is a big deal, you can always version the attribute off. Its not a language attribute, you can mock UDA's that the compiler won't recognize conditionally.
Sep 03
prev sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On Monday, 21 July 2025 at 02:03:54 UTC, Paul Backus wrote:
 On Monday, 21 July 2025 at 00:31:10 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 Problem is, some people believe it shouldn't. Hence this 
 proposal to give those that want that "reliability" control to 
 make it so.
If there are people who believe that (a) the struct instance should be allocated with the GC, but (b) the GC should not be responsible for its cleanup, then those people are wrong, plain and simple. On the other hand, if people want to assume control over both the allocation and the cleanup, that's already possible with existing language features.
Not calling destructors at the end of the scope that a variable is declared in is a recipe for disaster. For example: ```d { auto conn = lockConnection(); arr.map!(val => conn.send(val)).each; // allocates `conn` into a closure, because reasons. } { auto conn2 = lockConnection(); // if conn.dtor not run, then this is a deadlock. } ``` The better option is to not make arbitrary decisions that affect semantics of RAII. My suggestion for fixing this problem is to explicitly allocate the struct on the heap, and allow the compiler to merge that into a closure if you want to optimize that allocation (probably not worth it, but if you feel it's important, go for it). Note that erroring when allocating a struct with a destructor used to be an error, but was made not an error with a "hack" that was supposed to be temporary, but just was left there. -Steve
Aug 07
next sibling parent Paul Backus <snarwin gmail.com> writes:
On Friday, 8 August 2025 at 04:28:32 UTC, Steven Schveighoffer 
wrote:
 On Monday, 21 July 2025 at 02:03:54 UTC, Paul Backus wrote:
 On Monday, 21 July 2025 at 00:31:10 UTC, Richard (Rikki) 
 Andrew Cattermole wrote:
 Problem is, some people believe it shouldn't. Hence this 
 proposal to give those that want that "reliability" control 
 to make it so.
If there are people who believe that (a) the struct instance should be allocated with the GC, but (b) the GC should not be responsible for its cleanup, then those people are wrong, plain and simple. On the other hand, if people want to assume control over both the allocation and the cleanup, that's already possible with existing language features.
Not calling destructors at the end of the scope that a variable is declared in is a recipe for disaster. For example: [...]
Ok, I can see how this could be a footgun, especially given the limitations of the compiler's escape analysis. Ideally, we would only allocate closures for variables that _actually_ escape their scope, and it wouldn't be a surprise when it happened, but as long as we're stuck with these "defensive" closures, it's probably better to make everything explicit.
Aug 08
prev sibling parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 08/08/2025 4:28 PM, Steven Schveighoffer wrote:
 Not calling destructors at the end of the scope that a variable is 
 declared in is a recipe for disaster.
 
 For example:
 
 ```d
 {
     auto conn = lockConnection();
     arr.map!(val => conn.send(val)).each; // allocates `conn` into a 
 closure, because reasons.
 }
 
 {
     auto conn2 = lockConnection(); // if conn.dtor not run, then this is 
 a deadlock.
 }
 ```
 
 The better option is to not make arbitrary decisions that affect 
 semantics of RAII.
Doing this is also a disaster. Inject any copy into heap memory, into any position before an expression/statement. This may be done in another function that accepts it by-ref. ```d __gshared Connection global; ... global = conn; ... ``` I.e. ```d { auto conn = lockConnection(); global = conn; arr.map!(val => conn.send(val)).each; // allocates `conn` into a closure, because reasons. } { auto conn2 = lockConnection(); // if conn.dtor not run, then this is a deadlock. } ``` Note: while I am using a global, any heap memory will exhibit this behavior. Depending upon how the struct is implemented you will get one of these behaviors: 1. Do nothing. Heap variable is both locked and unlocked depending upon the call stack, and depending upon implementation this may never be able to be used. 2. Ownership transfer (unique). Later operations will fail, but won't look like it should. 3. Disable copying and rely on return value optimization. The assignment will fail, but you won't be able to put it into stack memory that isn't by-ref. Acceptable solution. 4. Detect copy and unlock. Highly error prone at runtime, I've had this issue with my error types. The only solution as a library author I can recommend is to disable copying. As a user, I would hate this due to past experience anything that enables error at runtime behavior from compiling. But there is a fifth option, which is what I am proposing. 5. Disable going into heap memory. This gets you all of 3, but also allows you to copy it around within the stack, including into pointers. Ideally we'd also have escape analysis to guarantee going down only and treat it as a borrow to prevent multiple locking. I do not like playing whack-a-mole on symptoms of a problem. And this issue for closures appears to be exactly that, a symptom of a much bigger problem. Worth noting that doing nothing is equivalent to what you are proposing in the above example. All you've done is closed one hole in a sieve.
Aug 08
prev sibling next sibling parent Ogi <ogion.art gmail.com> writes:
On Sunday, 20 July 2025 at 17:04:11 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 If a struct destructor is annotated with an attribute 
 `` stackonly`` it may only be called if the ``this`` pointer is 
 allocated on the stack. It does not overload.

 ```d
 struct RAII {
     ~this()  stackonly {}
 }

 void foo() {
     RAII* gc = new RAII; // Error: RAII can only be cleaned up 
 if it is on the stack
     scope RAII* stack1 = new RAII; // ok
     RAII stack2 = RAII(); // ok
 }
 ```
We used to have [scope classes](https://dlang.org/deprecate.html#scope%20as%20a%20type%20constraint) but they were deemed “a quirk in the language without a compelling use case” and deprecated.
Aug 08
prev sibling parent Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Sunday, 20 July 2025 at 17:04:11 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 If a struct destructor is annotated with an attribute 
 `` stackonly`` it may only be called if the ``this`` pointer is 
 allocated on the stack. It does not overload.
I guess “Prevent struct going into heap memory” (the title of this thread) isn’t what you’re suggesting in the post. However, struct`. It cannot be captured by closures and it cannot be boxed (heap allocated and possibly converted to `object`).
Sep 09