This code snippet illustrates the problem. This would compile on 2.067.1:
1) ---------------------
import core.stdc.stdlib;
struct S
{
~this() {}
}
S foo(void* p = alloca(1234))
{
return S();
}
int main()
{
foo();
return 0;
}
------------------------
With a little change to "main" it compiles again:
------------------------
void main()
{
S s = foo();
}
------------------------
Both the removal of a return statement and the storage into an unused variable are now necessary to work with alloca() in this fashion.
Here is another (complete) test case where RAII and alloca() can't be used in the same function.
2) ---------------------
int main()
{
import core.stdc.stdlib;
struct S { ~this() nothrow {}}
S s;
alloca(1234);
return 1;
}
------------------------
The use case of 1) is to enable a called function to allocate dynamic memory from its caller's stack or use malloc() if the memory requirement is large. The return value is then a RAII struct that - upon destruction - calls free() iff malloc() was used and does nothing if alloca() was used. This idiom allows for concise code that executes extremely fast and uses the least possible amount of stack memory, but must not be used in loops because alloca()'s allocations can only be released by returning from a function. It is a foundation of my "fast" library on GitHub.
This broke once before due to the inliner: https://issues.dlang.org/show_bug.cgi?id=13427
The test case added back then can probably just be extended to cover this new case.
Comment #2 by dlang-bugzilla — 2015-09-14T00:16:53Z
(In reply to Marco Leise from comment #1)
> Digger found this commit in 2.069:
> https://github.com/D-Programming-Language/dmd/commit/
> 4f0a458f6072a281892f3d26fdd8aeb30d250c03
>
> Since this can not be the commit that broke 2.068 I assume that it broke,
> got fixed unintentionally and then broke again. Something seems to be fuzzy
> about alloca() handling in the front-end.
Digger pointing at a merge commit means it was broken on another branch (which is why Digger stopped on the merge commit). To find the real commit, you'll have to bisect the proper branch (i.e. if you were bisecting master, you'll need to bisect stable).
But the error message seems correct, default arguments are passed from the caller site and b/c of the destructor a try catch block is needed around statements that might throw.
> This idiom allows for concise code that executes extremely fast and uses the least possible amount of stack memory
Not exactly sure why alloca doesn't work with EH handling, but I guess we need fixed addresses for variables to unwind them.
You could pass in a buffer to avoid alloca.
ubyte[1234] buf = void;
foo(buf.ptr);
It's even possible to wrap the buffer with the function return value.
auto withBuf(size_t n, alias func, Args...)(auto ref Args args)
{
static struct Result
{
ubyte[n] __buf;
ReturnType!func val;
alias val this;
}
Result res = void;
emplace(res.val, func(res.ptr, args));
return res;
}
I'll still try to find out whether 2.068.1 broke something unintentionally.
Comment #5 by k.hara.pg — 2015-09-15T02:38:38Z
It's intentionally introduced in:
https://github.com/D-Programming-Language/dmd/pull/5003
To fix wrong-code issue 14708.
Today, in Win64 and all Posix platforms, dmd uses exception handling table, and it doesn't support using alloca in a function that contains try-finally statement.
Comment #6 by bugzilla — 2015-09-16T02:40:49Z
> Not exactly sure why alloca doesn't work with EH handling
Because alloca shifts things around on the stack when it adds space, and the generated EH tables do not account for that.
The only real fix for this is to implement official Elf-style exception handling, which we should do anyway at some point. The fact that this was compiling on early compilers doesn't mean it was working.
Comment #7 by Marco.Leise — 2015-09-18T03:48:11Z
(In reply to Kenji Hara from comment #5)
> Today, in Win64 and all Posix platforms, dmd uses exception handling table,
> and it doesn't support using alloca in a function that contains try-finally
> statement.
I don't know how exactly exception handling works in D and my mental image currently is that as long as there is a dtor call pending that needs to run when an Exception interrupts the function's execution, alloca() must not be in effect.
Under the premise that my assumptions hold alloca() would work in following cases:
A) No statement could throw while the dtor call is pending. In fact the function is 'nothrow':
-------------------------------
import core.stdc.stdlib;
struct S { ~this() nothrow {} }
S foo() nothrow { return S(); }
void main() nothrow // Worked in 2.067, regressed in 2.068
{
void* p = alloca(1234);
foo();
}
-------------------------------
B) The dtors have already been run, by the time alloca() is reached:
-------------------------------
import core.stdc.stdlib;
struct S { ~this() {} }
S foo() { return S(); }
void main() // Worked in 2.067, regressed in 2.068
{
{
foo();
}
void* p = alloca(1234);
}
-------------------------------
(In reply to Walter Bright from comment #6)
> The fact that this was compiling on early compilers doesn't mean it was working.
No doubt there is a reason for this fix that introduced the regression. It's just not my code :D. I still believe that for my use case 2.067 did the right thing, otherwise I would have dealt with segfaults earlier, right? For example take this line here that broke in 2.068: https://github.com/mleise/fast/blob/v0.2.1/source/fast/internal.d#L136
`wcharPtr` has as a default parameter an alloca() call and returns a struct with a dtor. From this struct I extract a character and return immediately. At no time can an exception be thrown, so - I assume - no try-finally is ever inserted and therefore no warning about mixing exception handling with alloca().
You can run this code (tested on Linux/amd64) with dmd-2.067.1 and dub by typing "dub fetch fast && dub run fast".
(In reply to Martin Nowak from comment #4)
> You could pass in a buffer to avoid alloca.
> ...
Yes, but dynamic stack memory allocation is an entirely different thing. Sure one could always return blocks of 1, 2 or 4 KiB, but then you quickly move into cold cache and eat stack space faster than necessary. Now when working with strings the requirement is often only a few bytes, but alloca may be feasible for up to, say 1024. The way I use it is to set a limit for it and switch to malloc like this (simplified):
`required > 1024 ? malloc(required) : alloca(required)`
With the changes in 2.068, alloca() can't easily be used in functions that use RAII and return something, which is frequent in D.
P.S.:
I could fix the offending line in my code by changing it like this:
// BEFORE
benchmark ("cstring.wcharPtr", () { return wcharPtr!pathname.ptr[pathnameWStringLength]; })
benchmark ("cstring.wcharPtr", () { wchar result; { auto buf = wcharPtr!pathname; result = buf.ptr[pathnameWStringLength]; } return result; })
// AFTER
But this was supposed to be an concise/efficient way to provide 0-terminated whcar* to external libraries, not an exercise in obfuscated coding!</rant>