Skip to content

Commit 5c50af3

Browse files
committed
Use unsafe casting hack to make runtime async compatible IL
1 parent a86f88c commit 5c50af3

File tree

5 files changed

+56
-8
lines changed

5 files changed

+56
-8
lines changed

benchmarks/CSharpTaskBenchmarks/CSharpTaskBenchmarks.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<Nullable>enable</Nullable>
66
<IsPackable>false</IsPackable>
77
<EnablePreviewFeatures>true</EnablePreviewFeatures>
8-
<!-- <Features>$(Features);runtime-async=on</Features> -->
8+
<Features>$(Features);runtime-async=on</Features>
99
<NoWarn>$(NoWarn);SYSLIB5007</NoWarn>
1010
</PropertyGroup>
1111
</Project>

benchmarks/FSharpBenchmarks/AsynchonousCompletionBenchmark.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ open System.Threading.Tasks
1111
open IcedTasks
1212

1313
open System.IO
14+
open System.Runtime.CompilerServices
1415

1516

1617
[<AutoOpen>]
@@ -83,6 +84,9 @@ module AsyncHelpers =
8384
return 100
8485
}
8586
#if NET10_0_OR_GREATER
87+
[<MethodImpl(MethodImplOptions.Async)>]
88+
89+
// [<MethodImpl(8192s)>]
8690
let fsharp_tenBindAsync_TaskBuilderRuntime () =
8791
IcedTasks.Polyfill.TasksRuntime.TaskBuilder.task {
8892
do! taskYield ()

examples/ILSpySamples-CSharp/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33

44
// See https://aka.ms/new-console-template for more information
5-
Console.WriteLine("Hello, World!");
5+
Console.WriteLine(Thingy.DoThingAsync().GetAwaiter().GetResult());
66

77

88

99
public class Thingy
1010
{
11-
public async Task<int> DoThingAsync()
11+
public static async Task<int> DoThingAsync()
1212
{
1313
await Task.Yield();
1414
return 42;

examples/ILSpySamples/Library.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
open System.Threading.Tasks
44
open System.Collections.Generic
55

6-
76
module TaskRuntime =
87
open IcedTasks.Polyfill.TasksRuntime
98
open System.Runtime.CompilerServices
@@ -77,3 +76,6 @@ module Main =
7776

7877
let main _argv =
7978
TaskRuntime.doThing().GetAwaiter().GetResult()
79+
|> printfn "Result: %d"
80+
81+
0

src/IcedTasks/TaskBuilderBase_Net10.fs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ open System.Runtime.CompilerServices
77
open Microsoft.FSharp.Core.CompilerServices
88
open System.Collections.Generic
99
open System.Threading
10+
open System
11+
12+
#nowarn "42"
1013

1114
module internal AsyncHelpers =
1215

@@ -19,6 +22,43 @@ module internal AsyncHelpers =
1922
let inline awaitAwaitable awaiter =
2023
awaitAwaiter (Awaitable.GetAwaiter awaiter)
2124

25+
(*
26+
A big comment explaining the unsafe cast hack
27+
28+
Typically, F# would want to generate IL like this for returning a Task<int> from a method:
29+
30+
.method public static class [System.Runtime]System.Threading.Tasks.Task`1<int32>
31+
fsharp_tenBindAsync_TaskBuilderRuntime() cil managed async
32+
33+
... stuff in between
34+
35+
36+
IL_01e2: call class [System.Runtime]System.Threading.Tasks.Task`1<!!0> [System.Runtime]System.Threading.Tasks.Task::FromResult<int32>(!!0)
37+
IL_01e7: ret
38+
39+
However as noted in https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md
40+
41+
> Async methods also do not have matching return type conventions as sync methods. For sync methods,
42+
> the stack should contain a value convertible to the stated return type before the ret instruction.
43+
> For async methods, the stack should be empty in the case of Task or ValueTask, or the type argument in the case of Task<T> or ValueTask<T>.
44+
45+
Which means we can't return a Task<int> in the IL, we need to return int directly from the async method.
46+
To achieve this, we use an unsafe cast hack to cast the int directly to Task<int> to make the compiler happy and emit the correct IL.
47+
48+
With this as the return value, the IL generated is:
49+
50+
IL_0044: call instance !0 valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32>::GetResult()
51+
IL_0049: ret
52+
53+
We have the ValueTask because of either .Return or .Zero returning a ValueTask
54+
55+
*)
56+
57+
module internal Unsafe =
58+
let inline cast<'a, 'b> (a: 'a) : 'b =
59+
60+
(# "" a : 'b #)
61+
2262
type TaskBuilderBaseRuntime() =
2363

2464
member inline _.Return(value: 'T) =
@@ -195,7 +235,7 @@ type TaskBuilderRuntime() =
195235

196236
member inline this.Run([<InlineIfLambda>] f: unit -> 'a) : Task<'T> =
197237
AsyncHelpers.awaitAwaiter (f ())
198-
|> Task.FromResult
238+
|> AsyncHelpers.Unsafe.cast
199239

200240
/// Used to force type inference to prefer Task<_> for parameters of functions using the build
201241
member inline _.Source(task: Task<'T>) = Awaitable.GetTaskAwaiter task
@@ -212,9 +252,11 @@ type BackgroundTaskBuilderRuntime() =
212252
&& obj.ReferenceEquals(TaskScheduler.Current, TaskScheduler.Default)
213253
then
214254
AsyncHelpers.awaitAwaiter (f ())
215-
|> Task.FromResult
255+
|> AsyncHelpers.Unsafe.cast
216256
else
217-
Task.Run<'T>(fun () -> AsyncHelpers.awaitAwaiter (f ()))
257+
Task.Run<'T>(Func<'T>(fun () -> AsyncHelpers.awaitAwaiter (f ()))).GetAwaiter()
258+
|> AsyncHelpers.awaitAwaiter
259+
|> AsyncHelpers.Unsafe.cast
218260

219261

220262
/// Used to force type inference to prefer Task<_> for parameters of functions using the build
@@ -226,7 +268,7 @@ type ValueTaskBuilderRuntime() =
226268

227269
member inline this.Run([<InlineIfLambda>] f) : ValueTask<'T> =
228270
AsyncHelpers.awaitAwaiter (f ())
229-
|> ValueTask.FromResult
271+
|> AsyncHelpers.Unsafe.cast
230272

231273

232274
/// Used to force type inference to prefer ValueTask<_> for parameters of functions using the build

0 commit comments

Comments
 (0)