-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Proposal]: Structural Delegates #106582
Comments
This would be a massive feature request, rather than API request. I'm positive to this, but I'd hope we can include more if we are making changes to this. |
I am not sure that the need for this feature justifies the required work to design and implement it. The request already mentions metadata changes and three quite major special cases for the JIT, the GC and the type system. With function pointers being always available for the performance-minded people that don't care about capturing objects or unloadability, I cannot see what niche struct delegates would fill. Some of the ideas described here can be extracted into individual feature suggestions:
|
I think the way for successful structural delegates is a generalized shape provided by an interface abstraction (IDelegate, IAction, IFunc, etc.) that users can provide struct implementations for and constrain generic arguments on. Then Roslyn could target that by emitting anonymous struct-based delegate implementations for methods eligible for those. To better enable scenarios that are currently replicated by hand e.g in TensorPrimitives. Notably, function pointers are not a performance optimization in such case as they do not enable monorphization and subsequent inlining of delegate calls. Nor they enable struct or ref struct closures. |
I understand that static genericity on functions instead of data types is sometimes needed, but I am not sure if it's a good idea to make this easily accessible by elevating it to a first-class language feature. I am concerned by its downsides (increase in generated machine code size and fragility when used across logical modules) and its potential for abuse which I have seen happening in C++.
How common do you believe are the cases where this would be beneficial? I don't personally believe that merely low-level code like |
Very true. I just worked from the issue template.
Can you elaborate? |
Yeah, I kinda figured that would be the reaction to this proposal.
It's a similar niche to ref structs, possibly wider reaching, since just about everyone uses delegates, while not everyone uses ref structs. Considering the amount of effort put into improving performance and usability of safe managed code in .Net recently, this proposal is along the same trajectory, and shouldn't be dismissed just because it's a large amount of work, IMO.
That looks like an issue that has been open for 9 years with no action. #4331 This proposal is a fresh start from scratch, new types, no worries about breaking anything.
I'm not sure where that came from. This proposal has absolutely nothing to do with an invoke operator.
Those were mentioned, not because it may or may not be possible to exclude them on existing delegates, but just to highlight that the new types absolutely will not support them. |
That looks like the functional interfaces proposal. dotnet/csharplang#3452 That is similar to this proposal, but serves a different purpose. Namely, it can achieve zero allocation by using variable-sized structs, while this is a fixed-size type. |
Does this essentially make |
I think this would be the preferred delegate type to use in most situations, but I wouldn't say those would be obsolete. Tons of APIs exist today that use those delegate types, and those aren't going anywhere. If you want to use the multi-cast feature of delegates (C# |
The lowest effort kind of improvement in this area comes down to simply introducing the aforementioned interfaces and teaching Roslyn that method invocation operator applies to types implementing them. This can be subsequently augmented with Roslyn learning to generate anonymous struct and ref struct1 delegates with respective closure types, possibly offering better UX with generic arguments displayed as e.g. Once done, this could open up the possibility to introduce value-delegate taking method overloads to At the end of the day, this is but one of many possible ways to improve delegate/higher order func/lambda story, and as we've seen with "Runtime handled Tasks", a more out-of-box proposal could be suggested. Footnotes |
Finally have time to leave my words here. With stack promotion finally being a thing, it's now less important for being struct or class. If we don't provide extra support for reflection etc, structs of 2-pointers size would be OK. I'm more interested about how can we provide implementation about this. For nominal types, the pain is that we can't declare one method that satisfies different arities. Without changing this, the implementation of structural delegates can only be runtime magic. If we have variable arity support, we may declare them in ordinal managed code. Hypothetical syntax: struct StructuralDelegate<..TArgs, TReturn>
where TReturn : allows ref struct, allows void
{
private object? target;
private delegate*<object?, ..TArgs, TReturn> fnptr;
public TReturn Invoke(..TArgs args) => fnptr(target, args);
} |
@huoyaoyuan would you mind elaborating a bit on this or just providing some link with more information about it? It is the first time I hear about it so I'm curious. |
See more at #103361, and more follow-up PRs. Note that I'm not saying that allocation is not a problem. It's just having different impact now. Large structs have their issues. |
The idea is great, but I don't think such thing can be implemented in the proposed way. |
Why the downvote then?
I'm not sure what you mean. I proposed to add new IL metadata. |
I really like the idea, and I think it deserves its own proposal. I would love to be able to write a method that can return |
I'm sure I'm missing something... I thought there would already be a proposal for variadic generics, but I searched both here and on I would've assumed folks would be proposing that ever since generics was introduced 😅 Maybe it just got stuck in discussions and was never promoted to an issue? Would definitely be nice to finally stop needing to maintain dozens of variations of |
Variadics don't necessarily translate well to generics. Despite generics and templates having similar syntax, they are not the same thing and the implications of them are drastically different. This is particularly relevant as it pertains to the general ABI (Application Binary Interface) of an exposed API. Outside of templates, variadic functions themselves necessitate a different calling convention and specialized support for parameters. There is no support for variadic returns on concrete methods even in C++. The reason variadic returns look like they exist for templates is because templates don't really exist, they're "erased" at C++ compilation time. That is, they're just symbols that get specialized based on their usage and synthesize types as part of compilation. You can't "export" them from a library, the non-specialized implementation needs to exist in the header itself, you can't do Versioning IL metadata is also incredibly breaking and we want to avoid it wherever possible; so if you really want a feature then the best approach is to find a way it can be represented without needing to go and break the entire ecosystem, such as by using attributes or other mechanisms that let the runtime and languages work with it using the existing support. In some cases there might not be an obvious alternative and it may require such revisions to IL, but that also makes it drastically less likely to happen in the near term. |
To make it more clear: I'm not talking about va_args in C, or managed va_args in IL/managed C++. Instead, I'm requesting an general IL/C# syntax for "any signature". The signature is applicable to any calling convention because the using methods will just pass every arguments and returns as-is. It's effectively using signatures as generic parameters. It's a goal of higher-kind generic, and will naturally cover many runtime magics for delegates. |
@tannergooding I'm aware of the differences between C++ templates and the C# generics system. I wasn't trying to make any point about it being easy or even feasible for that matter to do variadic generics in C#, but just expressing surprise in not seeing a proposal case anywhere (not even a closed one). I think anyone would immediately frown upon seeing this for the first time, and would immediately jump to such proposal: |
I was simply trying to give some suggestions on how to approach this in a way that would give the highest possibility of a solution being found. If the community wants some variadic generics like support, then someone should start by writing up proposal which covers how it could integrate with existing IL metadata in a pay for play mechanism and some basics on how it could reasonably be handled in reflection, the type system, etc. |
Tbh, I don't expect this feature to be implemented any time soon. Besides the IL revisions, there is also the atomic r/w of 2 pointers which has been requested for 8+ years, and new GC tracking of managed function pointers. This is a chance to learn from 22 years of imperfect delegates, and to do it right from the ground up. I wouldn't want to shoe horn it into existing functionalities just to get it done faster if it's not the best it can be. |
We can't require atomic r/w of 2 pointers, its not guaranteed to exist. It's one of the reasons we can't trivially expose a BCL API for it, although a hardware intrinsic per platform might be feasible. |
It doesn't have to be an API only for 2 pointers. The |
I found this old issue on roslyn repo. dotnet/roslyn#5058 Not really any newer issues since then, and LDM would not likely adopt the proposed solution (hacky, compiler-driven). Probably more likely to gain traction as a runtime feature. |
Background and motivation
Motivating csharplang discussions: dotnet/csharplang#8343 dotnet/csharplang#7785
Today, delegates are nominal types, and delegates with matching signatures are not interchangeable.
Func<T, bool>
!=Predicate<T>
. It would be ideal to be able to declare delegates by their signature rather than by their nominal type, similar to how function pointers can be declared.delegate<T, bool>
(or whatever syntax C# decides to go with).Moreover, at a fundamental level, delegates need not be more than an object reference + a function pointer. The existing class-based delegates have a lot more baggage, making them less efficient than they theoretically could be.
Details
The runtime adds new structural delegate types, along with new IL metadata to use them.
A delegate declared in assembly A
delegate<T, bool>
and a delegate declared in assembly Bdelegate<T, bool>
are the exact same type, and therefore are identity convertible.delegate<ref T, void>
can be used also.System.Delegate
type already exists, so a new base type can be added.Yes, structural delegates are struct-based instead of class-based. It essentially looks like this:
These new structural delegates do not support Begin/EndInvoke or multi-cast like existing delegates.
To avoid possible tearing of the struct due to thread races, each read/write of a structural delegate will be made atomic by the runtime (this may be blocked by #105054 or #31911). If a struct contains structural delegate fields (or nested struct with structural delegate fields), copying that struct requires each structural delegate field to be copied atomically (the entire struct copy need not be atomic). As an optimization, the atomic r/w could be relaxed if the target is on the stack instead of the heap.
Additionally, to ensure that these structural delegate types are "safe" managed types, the GC should be taught to specially treat the function pointer as an ALC reference.
These types should also be specially treated similar to
Nullable<T>
, such that comparing tonull
becomes the same as comparing the pointer to zero, and boxing a default value results in a null object. With that in mind,Nullable<delegate<>>
may also need to be treated specially.There could also be additional APIs to convert to/from existing delegates, or bypassing the atomic r/w with Unsafe, and other APIs that exist for current delegates, but those can be left to future proposals if this ever goes anywhere.
Feature Usage
Alternative Designs
Structural delegates are class-based like existing delegates. This avoids the issue with atomic r/w and ALC reference, but also adds allocations and other overhead.
Risks
Enforcing atomic r/w could impact performance of shared generics in AOT runtimes.
The text was updated successfully, but these errors were encountered: