[Proposal] Sync-async method binding #8911
Replies: 2 comments 8 replies
-
Having the presence of an attribute be used to generator code that is based off of user code is exactly the realm of a source generator. You should be able to do this today without needing a language change or any additional features from the compiler :-) |
Beta Was this translation helpful? Give feedback.
-
Well nobody really has to always support both workflows. Many modern apis are currently either sync or async and not both. There are many old ones and Stream is a an example of that, yes, but honestly it is at the same time a terrible example to base language features on. Every now and then you have to override a ton of stuff and think of "what if there was a better way? If only language could so this and that...". But language should not cover pain points of old api designs. It should instead open possibilities to create new ones which are both faster and more ergonomic... Until they're legacy again. Honestly I don't think you're making these stream overrides every day. But if you do there is a Copilot. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
For the long time since the introduction of
async
methods we have to write synchronous overloads for asynchronous methods to support both workflows. However it means that we have to write double amount of the code which differs only byawait
keyword.Example:
These methods almost always have to be almost same except awaiting method(blocking for synchronous, await for asynchronous). This proposal's purpose is to eradicate this code duplication by enforcing method binding. Forementioned methods are always logically bound, i.e. when you use
stream.Read()
you expect thatstream.ReadAsync()
should do exactly the same, just in non-blocking way. This assumption allows us to make compilation of synchronous methods dependent on their asynchronous counterpart.Proposal source code example, it compiles in exactly same code as in example before:
At the moment proposal is expected to perform at MSIL compile-time so it's mostly C#-limited feature. Later it can be moved to JIT compile-time, so we can reduce our binary size as well since only one instance of the bound methods would be present in a binary.
When project is compiled and compiler encounters method
Foo.WriteAsync()
, it checks the existence ofSync
attribute. Since it is present, compiler emits a synchronous counterpart ofWriteAsync()
, where name is replaced bySyncAttribute.NameOverride
or initial method's name withoutAsync
suffix.Return type is decided by the return type of initial method: if task-like type is typed(
Task<T>
,ValueTask<T>
and etc), then return type isT
, otherwise(Task
,ValueTask
and etc) - void.Any parameter marked with
AsyncOnly
(we can introduce theSyncOnly
counterpart as well, but I don't see any reason for it now) is omitted from synchronous method. The order of parameters is saved as is.For the all method's code compiler should apply asynchronous method's rules, i.e. no locks with awaits, no byref variables and etc. Even though it is considered that we will have a synchronous counterpart, rules should be the same since methods are expected to have same logical behaviour.
When compiler encounters an
await
in the code, it uses it as is inasync
case, but for thesync
case it should check if called method(stream.WriteAsync()
in forementioned case) is marked withSync
attribute. If the attribute is present, then compiler automatically replaces this call with synchronous counterpart, all parameters are mapped from initial async call. In our casecancellationToken
would be omitted from synchronous call since it would be markedAsyncOnly
in called method. If an expession was used as a parameter forcancellationToken
, i.e.stream.WriteAsync(arr, LogAndReturn(cancellationToken))
, thenLogAndReturn()
is not called in synchronous counterpart, i.e. behaviour is alike withConditionalAttribute
.If awaited method is not marked with
Sync
, then compiler throws an error: "Method is not bound with a synchronous counterpart".Compiler should ensure that no
AsyncOnly
parameters are used in synchronous context.In case when we have to use different logic for sync and async cases, we can use next approach:
async
andsync
keywords placed in method body are just a syntax sugar that can be used to mark scope with corresponding rules, i.e. likeunsafe
. When compiler compilesWriteAsync()
method, for theasync
counterpart it will ignore allsync
blocks and vice versa. When inside of async
orasync
block, compiler ignores any need forSync
attribute mapping of awaited methods and just compiles this code as is.Beta Was this translation helpful? Give feedback.
All reactions