State machines to stackalloc on the caller's stack #9512
-
Consider APIs like GetWindowTextW and GetWindowTextLengthW. To use them without allocations, you'd have to call both of them like so: public static unsafe partial class Win32 {
[LibraryImport("user32.dll")]
public static partial int GetWindowTextW(nint hWnd, char* lpString, int nMaxCount);
[LibraryImport("user32.dll")]
public static partial int GetWindowTextLengthW(nint hWnd);
public static unsafe void Main() {
nint hWnd = 0x1000;
var length = GetWindowTextLengthW(hWnd) + 1;
var buffer = stackalloc char[length];
GetWindowTextW(hWnd, buffer, length);
var span = new ReadOnlySpan<char>(buffer, length);
// and if we were to return the span - nope
}
} This takes a while, and the resulting span isn't easily usable if we were to implement a managed However, in C#, we're already using state machines to break a single method body into multiple compiled methods that get called in a specific order in the caller method's context, while the developer only sees one method and a few extra keywords. Consider What if we were to use a state machine to:
Like so: // Using `caller_stackalloc` to declare a method as a caller-stack-allocating state machine
public static caller_stackalloc ReadOnlySpan<char> GetLength() {
nint hWnd = 0x1000;
var length = GetWindowTextLengthW(hWnd) + 1;
// Addition two: 'caller_stackalloc' keyword
// We yield the `length` argument, receive the pointer from the caller, then proceed as if we use regular `stackalloc`
var buffer = caller_stackalloc char[length];
GetWindowTextW(hWnd, buffer, length);
}
public static void Main() {
ReadOnlySpan<char> text = caller_stackalloc GetLength();
} Which would be lowered to something like: public struct Allocator {
public int GetLength() {
nint hWnd = 0x1000;
var length = Win32.GetWindowTextLengthW(hWnd) + 1;
return length; // caller_stackalloc: yield
}
public void FillBuffer(char* buffer) { // caller_stackalloc: continue
GetWindowTextW(0x1000, buffer, GetLength());
}
}
public static void MainLowered() {
var allocator = new Allocator();
var length = allocator.GetLength();
var buffer = stackalloc char[length];
allocator.FillBuffer(buffer);
}
// The keywords/syntax/lowering are only for demonstration purposes, and are not a part of the proposal - they only demonstrate the idea This code sample omits state persistence in the allocator struct, and the fact that we will most likely have to return a dedicated "awaiter"-like struct rather than a I think this would be an amazing tool for native interop and performance-critical scenarios, and it would help with adoption of spans in scenarios where they're relevant, but cumbersome to use or even considered as overengineering. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
Ive seen methods like this implemented as var buffer = stackalloc char[fillBuffer(null)];
fillBuffer(buffer); Another way that I personally think is better than messing with the stack which can potentially overflow, is to just use a "scratch space" arena allocator (bump a pointer to allocate, reset it to deallocate) that you pass as an argument. In its simplest form it just looks like this: void* scratch = ... // allocate some memory on program/thread init. maybe wrap it in some struct if you like
void doStuff(void* scratch) {
var myText = getText(ref scratch); // ref so you observe the pointer bump
...
}
doStuff(scratch); // no ref so it "auto deallocates" after the call because you dont observe pointer bumps |
Beta Was this translation helpful? Give feedback.
-
Such code isn't a "correct" implementation and is prone to stack overflow for things like Beyond that, there isn't really a way to define the state machines (even with ref structs) in a way that allows the scoping and other lifetime requirements of the stack allocation to be properly tracked. It might be possible for the language to add such support, but it would be very complex for a very niche scenario where the alternative results in code that is overall safer, more maintainable, more robust, and more performant. Such an API should likely be exposed to the consumer as is, with a |
Beta Was this translation helpful? Give feedback.
Such code isn't a "correct" implementation and is prone to stack overflow for things like
edit
controls which can have text longer than what is "safe" to stack allocate.Beyond that, there isn't really a way to define the state machines (even with ref structs) in a way that allows the scoping and other lifetime requirements of the stack allocation to be properly tracked. It might be possible for the language to add such support, but it would be very complex for a very niche scenario where the alternative results in code that is overall safer, more maintainable, more robust, and more performant.
Such an API should likely be exposed to the consumer as is, with a
GetTextLength
and aTryGetTe…