I’m proposing a new rule that would go into the "other function rules" section. I did a pass over both the guidelines and didn’t see anything regarding coroutines.
This pattern is very dangerous because it looks normal like any other lambda but in fact it is super dangerous. The issue is that the coroutine itself will go out of scope after the first suspension point. This means that even value types will be operating on freed memory. Taking a strong reference to a class doesn’t help because the smart pointer itself goes away. Basically passing the values in by value instead of capturing them in the lambda makes it safe again. At that point it might as well just be a function instead of a lambda.
Additionally, when debugging one of the crashes that came from this unsafe usage it became known that the coroutine optimizer does odd things with coroutine lambdas. The result is that even with valid symbols the debugger can only dump partial call stacks. Anything prior to the coroutine lambda will fail to dump because a jump is used instead of a return statement. The result is that stack backtrace commands will just… stop after the coroutine lambda.
For all these reasons I’m proposing that we ban usage of this language construct because it is too dangerous. There are ways to make it safe (not actually awaiting, capturing by parameter) but those defeat the purpose of using a lambda. They are also vulnerable to “I’ll change this to a capture because passing by parameter is dumb” fixes in the future that would make them unsafe again.
The proposed guideline is below:
Do not use lambdas that are coroutines
Reason
Usage patterns that are correct with normal lambdas are hazardous with coroutine lambdas. The obvious pattern of capturing variables will result in accessing freed memory after the first suspension point, even for strong references and value types.
Example, Bad
const auto strongThis { get_strong() };
const auto lambda = [strongThis]() -> winrt::fire_and_forget
{
co_await winrt::resume_background();
// strongThis now points to freed memory, even though we took a strong reference
};
lambda();
Example, Better
const auto lambda = [](auto strongThis) -> winrt::fire_and_forget // take as by-value parameter instead of as a capture
{
co_await winrt::resume_background();
};
const auto strongThis { get_strong() };
lambda(strongThis);
Example, Best
Use a function for coroutines.
I’m proposing a new rule that would go into the "other function rules" section. I did a pass over both the guidelines and didn’t see anything regarding coroutines.
This pattern is very dangerous because it looks normal like any other lambda but in fact it is super dangerous. The issue is that the coroutine itself will go out of scope after the first suspension point. This means that even value types will be operating on freed memory. Taking a strong reference to a class doesn’t help because the smart pointer itself goes away. Basically passing the values in by value instead of capturing them in the lambda makes it safe again. At that point it might as well just be a function instead of a lambda.
Additionally, when debugging one of the crashes that came from this unsafe usage it became known that the coroutine optimizer does odd things with coroutine lambdas. The result is that even with valid symbols the debugger can only dump partial call stacks. Anything prior to the coroutine lambda will fail to dump because a jump is used instead of a return statement. The result is that stack backtrace commands will just… stop after the coroutine lambda.
For all these reasons I’m proposing that we ban usage of this language construct because it is too dangerous. There are ways to make it safe (not actually awaiting, capturing by parameter) but those defeat the purpose of using a lambda. They are also vulnerable to “I’ll change this to a capture because passing by parameter is dumb” fixes in the future that would make them unsafe again.
The proposed guideline is below:
Do not use lambdas that are coroutines
Reason
Usage patterns that are correct with normal lambdas are hazardous with coroutine lambdas. The obvious pattern of capturing variables will result in accessing freed memory after the first suspension point, even for strong references and value types.
Example, Bad
Example, Better
Example, Best
Use a function for coroutines.