-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Champion "Static Delegates" #302
Comments
@jaredpar I took a look at the proposal and maybe this is beyond my understanding, but if this is geared at interop i'm not really clear what the difference or synergy between this and an |
Could you please add some examples of how this might be used, and it's limitations? For example, in the proposal, it's stated that "Static Delegates would not work with existing APIs that use regular delegates". I'm reading that as I couldn't use this to improve the performance of: int Foo(int p, Func<int, int> f) => f(p); But could static delegates be used to provide a better performing version of |
@AlgorithmsAreCool, think of this as a type of It can be used for interop, but it can also be used as a type of lightweight delegate. |
@DavidArno. It wouldn't be useable as a drop in replacement in the same way that you can't just use You could convert from one to the other, so it could be 'used', but it would incur the cost of the allocation/etc. |
Could you provide a little code snippet showing the creation, passing (to a function) and invocation of a static delegate? Also why is it "static". Wouldn't |
NOTE: The LDM could decide to do this entirely differently, this is just how I imagine it 😄 As for declaration, I imagine exactly like a regular delegate, but prefixed with static delegate T ValueFunc<T>();
static delegate IntPtr WNDPROC(
[In] IntPtr hWnd,
[In] uint uMsg,
[In] IntPtr wParam,
[In] IntPtr lParam
); For creation, assignment, and passing, it would work just like a regular delegate:
class TestClass
{
ValueFunc<int> _valueFunc;
public TestClass(ValueFunc<int> valueFunc)
{
_valueFunc = valueFunc;
}
} You could not do multiple assignment, so |
Ah, all is made clear. The static was throwing me off. I like this and would use it. |
So reading Jared's main post on the subject in the other thread:
I guess this also precludes use as closures? |
@Joe4evr. Possible, however just because it only binds to static methods, doesn't mean it can't access instance fields, it just means you would need to be explicit about passing in Such as: static delegate void ValueAction<T>(T obj);
public class MyClass
{
private int _someField;
public static void SomeMethod(MyClass @this)
{
Console.WriteLine(@this._someField);
}
public void Method1()
{
ValueAction<MyClass> x = SomeMethod;
x(this);
}
} So lets take some code you might have today: public class MyClass
{
public void Method1()
{
int someLocal = 1;
ValueAction x = static () => Console.WriteLine(someLocal);
x();
}
} This gets translated (roughly) into: public class MyClass
{
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int someLocal;
internal void <Method1>b__0()
{
Console.WriteLine(someLocal);
}
}
public void Method1()
{
var CS$<>8__locals0 = new <>c__DisplayClass0_0();
CS$<>8__locals0.someLocal = 1;
Action x = CS$<>8__locals0.<Method1>b__0;
x();
}
} You then might imagine the same being supported with static delegates: public class MyClass
{
public void Method1()
{
int someLocal = 1;
ValueAction x = static () => Console.WriteLine(someLocal);
x();
}
} The hard part here, is that the user declares At least without delving too much into the finer details, I could imagine something like the following working: static delegate void ValueAction();
public class MyClass
{
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public static ThreadLocal<<>c__DisplayClass0_0> threadData;
public int someLocal;
internal static void <Method1>b__0()
{
var data = threadData.Value;
Console.WriteLine(data.someLocal);
}
}
public void Method1()
{
var CS$<>8__locals0 = new <>c__DisplayClass0_0();
CS$<>8__locals0.someLocal = 1;
ValueAction x = <>c__DisplayClass0_0.<Method1>b__0;
<>c__DisplayClass0_0.threadData.Value = CS$<>8__locals0.someLocal;
x();
}
} I think the primary issue with the above approach would be memory overhead for stray threads, but I honestly haven't put too much thought into it right now 😄 |
Looks like this was discussed briefly in LDM here: https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-05-16.md#static-delegates |
So if an API takes a "static delegate" then I can't capture any extra state or parameters? This is the problem with function pointers in C - you can't pass anything extra and it forces you to use global variables. You can't pass any extra You might make an API taking a static delegate thinking nobody will probably ever need to pass in anything because this is something that should ideally only depend on the parameters but one day someone will need to do that and they're gonna have to use globals. Which is not only ugly and terrible but also can't be made thread-safe. |
Not via closures. The point is to be as light-weight as function pointers, which precludes any of the signature rewriting or allocations that requires. But if you wanted to provide a mechanism to pass state you could design your API to accept that state explicitly and then return that within the function pointer as an argument, similar to how events/callbacks were often designed prior to closures. |
But the problem is the API won't be designed that way because they will not know what I need. This is why globals are used everywhere in C and it's terrible. This problem was even solved in C++ without the need for allocations, let alone a garbage collector. It's very efficient and they're not afraid to use lambdas with captures in the most performance critical piece of code, but in C# we are going back to function pointers? Can't we do better? |
Someone with better knowledge of C++ lambdas could correct me, but IIRC you can't actually return the a lambda created in a method without some kind of allocation (as they are on the stack) and in the case of by-reference captures the onus is on the developer to ensure that the lambda is not somehow invoked beyond the life of any of the captured values. Any capturing lambda has to be converted into an unnamed functor-like type. |
C++ solves this with lambdas by allocating the state on the stack and passing it by value (or reference depending on the lambda declaration). C# can take the exact same approach. State passing by value can be accomplished via structs. This is actually how local functions work: state capture without the need for allocations. Today though in C# there are two sources of heap allocations: the closure and the delegate. Once delegates are static the heap allocation can be removed hence there will be a reason to focus on the closure allocation. |
@jaredpar Any thoughs on timing of implementation? X.0 candidate looks like no candidate at all. |
That just means essentially "it would only be done in a major release and we'll figure out which one later". The complication with static delegates is that it requires runtime changes. Imagine this scenario:
If that last operation is allowed to succeed then now you have a type safe delegate that points to unloaded memory. Oops 😦 This needs to be addressed at some level in the runtime: either by preventing the AppDomain from being unloaded or by disallowing the delegate to be passed between AppDomain instances. In general our default is to pin language changes that depend on runtime changes to major releases. I think it has a decent shot at 8.0 though. |
This are good news 😄 |
I'm actually referring to the closure struct here, not the captured value. The captured value must be available to the method to which the
|
@jaredpar This first one ended up not really working because any sort of struct inheritance I could think of wouldn't support both downcasting and overriding functions. A // In source code
public static int LambdaUsingFunc(int a, Span<byte> span)
{
int result = Foo(b =>
{
int value = b ? 4 : 8;
a++;
return value + a + span[0];
});
return result + a;
}
public static int Foo(ValueFunc<bool, int> func)
{
return func.Invoke(true);
}
// As compiled by Roslyn
public static int LambdaUsingFunc(int a, Span<byte> span)
{
var closure = new Closure();
closure._a = a;
closure._span = span;
var funcStruct = new FuncStruct();
funcStruct.FuncPtr = &Closure.Lambda_1; // Or however you create a function pointer.
funcStruct.Closure = ref closure;
int result = Foo(funcStruct);
return result + closure._a;
}
[CompilerGenerated]
internal ref struct Closure
{
public int _a;
public Span<byte> _span;
public static int Lambda_1(Closure closure, bool b)
{
int value = b ? 4 : 8;
closure._a++;
return value + closure._a + closure._span[0];
}
}
// I'm not sure how this could work since structs can't inherit structs and don't have vtables
[CompilerGenerated]
internal ref struct FuncStruct : ValueFunc<bool, int>
{
public delegate*<Closure, bool, int> FuncPtr;
public ref Closure Closure; // Requires ref fields
public int Invoke(bool a)
{
return FuncPtr(Closure, a);
}
}
Do you mean something like this? // This could work with ref fields and ref struct constraints, but it makes the signatures for
// functions using a ValueFunc more complicated and could cause generic bloat
internal ref struct ValueFunc<TClosure, TIn, TOut> where TClosure : ref struct
{
public delegate*<TClosure, TIn, TOut> FuncPtr;
public ref TClosure Closure; // Requires ref fields
public TOut Invoke(TIn a)
{
return FuncPtr(Closure, a);
}
}
public static int Foo<TClosure>(ValueFunc<TClosure, bool, int> func)
{
return func.Invoke(true);
} Using // IntPtrs could be used. The compiler would make sure the types they point to are valid
public ref struct ValueFunc<bool, int>
{
private IntPtr FuncPtr;
private IntPtr Closure; // Points to a ref struct. GC wouldn't be an issue because
// the struct must live somewhere else on the stack.
public int Invoke(bool a)
{
// Make sure FuncPtr isn't null
// If Closure isn't null, call FuncPtr with Closure as the first argument,
// otherwise call FuncPtr without closure
}
} |
But what would you do with such a delegate containing a ref-struct closure? You couldn't store it anywhere so its basically just a glorified inversion-of-control scheme to write your algorithms differently. Not saying thats a useless thing but wanting to point at the fact that this is an entirely different kind of delegate incompatible to anything currently operating on |
@weltkante You'd use it in places where the delegate is only used during execution of the function it's passed to. This is fairly common in my experience. I gave an example a couple posts back, and I've seen people discussing its use for making more optimized LINQ-like functions. |
This is the crux of the problem with this approach. It's not workable because a |
@Thealexbarney Can't you already have LINQ-like methods acting on |
Mostly joking (this would be incredibly unsafe/complex, etc) but you could always emulate a vtbl. A vtbl in C/C++ is generally just (yes this is a COM like example, but it applies to both the MSVC++ ABI used by Windows and the Itanium C++ ABI used by Linux/macOS as well): struct IUnknown {
IUnknownVtbl* lpVtbl;
};
struct IUnknownVtbl {
public delegate*<IUnknown*, Guid*, void**, int> QueryInterface;
public delegate*<IUnknown*, uint> AddRef;
public delegate*<IUnknown*, uint> Release;
}; where the constructor for the "type" initializes the function pointers to the right addresses. The same principal could be applied to other cases where you want to have pseudo inheritance with structs. |
Yes but once again: how do you pass the closure around? Your example is a valid implementation of a COM v-table but it lacks any inherently associated data. The crux of this problem remains the same: how to get the closure from the frame it's created to the frame it's used. I've laid out the available options I'm aware of. |
Assuming this is a ref struct and the closure is only be passed down (and never stored elsewhere). It should be fine to just create a |
That's not fine though because it leaves a GC gap. Assuming the closure isn't used after the method invocation, which is a decent bet, then the GC will consider all of the data within the closure to be collectable. The |
Ah right. I was assuming the Closure was |
@Sergio0694 Struct enumerators would be a different thing. I recently saw some people talking about how a @jaredpar That wouldn't be an issue as long as the lifetime of the closure is at least as long as the |
That introduces two new technologies:
Even assuming those two existed and those problems were solved @tannergooding sample is still lacking. How do the dots get connected from the point the closure is created to the point that the closure is used? His comments essentially suggest strong typing in the form of unsafe ref struct RefStructDelegate {
void* Closure;
int Func<void*, int> Target;
} Then you can essentially have the following pattern: void Producer() {
Closure$ c = default;
c.span = ...;
RefStructDelegate d = default;
d.Closure = &c;
d.Target = Lambda$;
Consumer(d);
KeepAlive(c); // Somehow
}
unsafe int Consumer(RefStructDelegate d) {
return d.Target(d.Closure, 42);
}
unsafe int Lambda$(void* pClosure, int data) {
ref Closure c = Unsafe.ThatMethodWhichMakesPointersRefs((Closure*)pClosure));
return ...
} This has the further downside that the consumer must use |
Is there somewhere one can read up on the reason why this was rejected, and which alternatives are in discussion(if any)? |
I think the function pointers proposal overlaps with this so it makes no sense to implement this one as well? Work seems to have started on function pointers already. |
That is correct, this proposal was superseded by function pointers. |
Uh, it would be great if we do some cleanup/closing/tagging with such proposals to track the appropriate ones instead. |
@nxrighthere once function pointers has actually shipped this proposal will get closed out. Generally we don't close the main proposal or competing ones until the features have shipped. |
Got it, thanks. I think GitHub is really missing a sort of "issues chains/dependencies" or a feature like that to explicitly notice such cases. |
Then I was upset for nothing :D Thanks for the clarification |
Shouldn't the initial post be updated as Finalized and perhaps a line added pointing to the superceding feature as the reason? |
Once we ship yes. |
Superseded by https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/function-pointers.md. Closing out. |
Superseded by https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/function-pointers.md
The text was updated successfully, but these errors were encountered: