Skip to content
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

Closed
2 of 5 tasks
tannergooding opened this issue Mar 21, 2017 · 68 comments
Closed
2 of 5 tasks

Champion "Static Delegates" #302

tannergooding opened this issue Mar 21, 2017 · 68 comments
Assignees
Milestone

Comments

@tannergooding
Copy link
Member

tannergooding commented Mar 21, 2017

Superseded by https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/function-pointers.md

@tannergooding
Copy link
Member Author

FYI. @gafter. I think this one was skipped over after the proposal was merged. It is being championed by @jaredpar

@AlgorithmsAreCool
Copy link

@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 extern function is. Maybe that isn't even a sensible question...

@DavidArno
Copy link

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 Foo? If so, what would the signature look like?

@tannergooding
Copy link
Member Author

@AlgorithmsAreCool, think of this as a type of value delegate (in the same vein as ValueTuple compared to Tuple).

It can be used for interop, but it can also be used as a type of lightweight delegate.

@tannergooding
Copy link
Member Author

@DavidArno. It wouldn't be useable as a drop in replacement in the same way that you can't just use ValueTuple anywhere that you can use Tuple. One always exists on the heap, and the other could potentially exist on the stack.

You could convert from one to the other, so it could be 'used', but it would incur the cost of the allocation/etc.

@AlgorithmsAreCool
Copy link

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 struct delegate be a better moniker?

@tannergooding
Copy link
Member Author

tannergooding commented Mar 21, 2017

static delegate was just the name @jaredpar gave them in the original issue: #80. But, as everyone knows, computer programming only has two hard problems ;)

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:

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:

  • ValueFunc<int> valueFunc = Test;
  • var result = valueFunc();
class TestClass
{
    ValueFunc<int> _valueFunc;

    public TestClass(ValueFunc<int> valueFunc)
    {
        _valueFunc = valueFunc;
    }
}

You could not do multiple assignment, so valueFunc += Method; and valueFunc -= Method; would result in a compilation error.

@AlgorithmsAreCool
Copy link

Ah, all is made clear. The static was throwing me off.

I like this and would use it.

@Joe4evr
Copy link
Contributor

Joe4evr commented Mar 21, 2017

So reading Jared's main post on the subject in the other thread:

  1. It can only bind to static methods.
  2. Under the hood it is represented as a struct that has a single member of type IntPtr.

I guess this also precludes use as closures?

@tannergooding
Copy link
Member Author

@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 this.

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 ValueAction, but in order to pass in the captured state we need ValueAction<T>. So, provided an answer is given for the required transformation here, it should be possible.

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 😄

@MadsTorgersen MadsTorgersen modified the milestone: 7.2 candidate May 16, 2017
@tannergooding
Copy link
Member Author

@Neme12
Copy link

Neme12 commented Jan 29, 2018

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 this if it's not in the signature.

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.

@HaloFour
Copy link
Contributor

@Neme12

So if an API takes a "static delegate" then I can't capture any extra state or parameters?

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.

@Neme12
Copy link

Neme12 commented Jan 29, 2018

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?

@HaloFour
Copy link
Contributor

@Neme12

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.

@jaredpar
Copy link
Member

This problem was even solved in C++ without the need for allocations, let alone a garbage collector.

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.

@4creators
Copy link

Once delegates are static

@jaredpar Any thoughs on timing of implementation? X.0 candidate looks like no candidate at all.

@jaredpar
Copy link
Member

@4creators

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:

  • AppDomain 1: Loads Util.dll and creates a static delegate to a method inside it.
  • AppDomain 1: then passes that static delegate to AppDomain 2. A static delegate is just implemented essentially as an IntPtr and that's serializable.
  • AppDomain 2 tries to unload AppDomain 1

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.

@4creators
Copy link

I think it has a decent shot at 8.0 though.

This are good news 😄

@jaredpar
Copy link
Member

@Thealexbarney

I don't understand why usage of ref structs as generic type parameters would affect ref struct capture. The ref struct would be a member of the closure struct, not a parameter type for a Func.

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 Func<> refers. There are only limited ways to do this:

  1. Attach the state to the Target member of the System.Delegate instance. This is how closure capture works today. That is simply not an option though once we get into the capture of ref or ref struct types because the Target parameter forces a heap allocation.
  2. Pass the closure state as a argument to the Func<>. No heap capture is required here but it does mean that the Func<> in the signature needs to accept many different closure types. The only real options for doing for ref like capture is interfaces or generics

@Thealexbarney
Copy link

@jaredpar
I thought a little about the details of how it might work by creating simplified examples.

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 ref interface feature could work. From what I can tell that would require compiling a new version of Foo for every ref interface derived type it's called with, similar to generics.

// 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);
    }
}

Pass the closure state as a argument to the Func<>. No heap capture is required here but it does mean that the Func<> in the signature needs to accept many different closure types. The only real options for doing for ref like capture is interfaces or generics

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 IntPtr might work like in the original static delegate proposal. The struct would be created by the compiler, so it could verify the ValueFunc type parameters, FuncPtr and Closure types all agree with each other.

// 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
    }
}

@weltkante
Copy link

weltkante commented Feb 18, 2020

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 Action or Func delegates, because the existing delegate type implies that you can store it in a data structure. You'd have to write entirely new infrastructure to operate on those delegates (and it would be very restricted in what it can do because no storing the delegate anywhere).

@Thealexbarney
Copy link

@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.

@jaredpar
Copy link
Member

@Thealexbarney

// I'm not sure how this could work since structs can't inherit structs and don't have vtables

This is the crux of the problem with this approach. It's not workable because a struct can't have a vtable. In order to give a struct implementation defined behavior it essentially needs to either use an interface (explicit way to provide a v-table) or generics. That's why I listed those two particular issues as the real blockers for ref based capture.

@Sergio0694
Copy link

I gave an example a couple posts back, and I've seen people discussing its use for making more optimized LINQ-like functions.

@Thealexbarney Can't you already have LINQ-like methods acting on Span<T>-like types, as long as you define your own ref struct enumerators that go along with them? Hyperlinq is doing exactly this: it exposes extensions for Span<T> and ReadOnlySpan<T> similar to LINQ APIs, taking predicates and whatnot, then captures them in a ref struct enumerator and uses that to produce lazy results over the initial sequence. You're basically suggesting a way to achieve this without having to manually define your own ref struct enumerators every time, correct?

@tannergooding
Copy link
Member Author

It's not workable because a struct can't have a vtable

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.

@jaredpar
Copy link
Member

@tannergooding

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.

@tannergooding
Copy link
Member Author

tannergooding commented Feb 19, 2020

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 ref struct Closure and then store it alongside the function pointer as a Closure* (stored in a ref struct ValueDelegate) to the location on the stack.
It isn't "safe", but provided you follow the storage rules for a ref struct, there shouldn't be an issue functionally speaking as the Closure will live the entire time that the ValueDelegate is to live and not longer (and this will then just be working around dotnet/runtime#32060 not existing yet).

@jaredpar
Copy link
Member

It should be fine to just create a ref struct Closure and then store it alongside the function pointer as a T* (stored in a ref struct ValueDelegate) to the location on the stack.

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 T* isn't a strong reference hence it won't help here.

@tannergooding
Copy link
Member Author

Ah right. I was assuming the Closure was unmanaged which may not be the case.

@Thealexbarney
Copy link

@Sergio0694 Struct enumerators would be a different thing. I recently saw some people talking about how a static delegate-like concept would help with performance for operations that have a function as a parameter, but using yield return would still create a class.

@jaredpar That wouldn't be an issue as long as the lifetime of the closure is at least as long as the ValueDelegate. Something like KeepAlive could be used if needed.

@jaredpar
Copy link
Member

jaredpar commented Feb 20, 2020

@Thealexbarney

that wouldn't be an issue as long as the lifetime of the closure is at least as long as the ValueDelegate. Something like KeepAlive could be used if needed.

That introduces two new technologies:

  1. A KeepAlive that is usable for any arbitrary ref struct. Such a function does not exist and is not really authorable unless we implement the ref struct support for generics or interfaces (again the two issues I listed above).
  2. In order to get the lifetime guarantee that you specified the delegate type we pass down must be a ref struct as well. That is also not available today.

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 Closure*. Again though that doesn't work because every closure has a different type meaning the containing "value delegate" would also need a different type. To really make this work you essentially need to have the following setup:

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 unsafe code because the RefStructDelegate is itself unsafe. That really defeats the purpose of using Span<T> in the firs place. The language is much more likely going to prefer finding safe solutions to these problems vs. unsafe ones.

@MadsTorgersen MadsTorgersen modified the milestones: 9.0 candidate, Likely Never Apr 22, 2020
@jvbsl
Copy link

jvbsl commented May 18, 2020

Is there somewhere one can read up on the reason why this was rejected, and which alternatives are in discussion(if any)?
I would like to know why something so important to platform independency for bigger things than "Hello World" native interop was rejected.
I was pretty happy when this was discussed for 9.0 and hoped to have it with .Net 5 for implementation of OpenTK. As it is currently impossible to have a library be able to do native symbol resolving if a custom symbol loader is provided by the C interface. Which is impossible to avoid with OpenGL/OpenAL and Vulkan. It would be really nice to have more control over low level stuff like that, or possibility to use DllImport with custom symbol resolving. Now the only possible ways are writing code in IL or using Marshal.GetDelegateForFunctionPointer which is as far as I'm aware still a good bit worse on performance than calli, or DllImport.

@weltkante
Copy link

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.

@333fred
Copy link
Member

333fred commented May 18, 2020

That is correct, this proposal was superseded by function pointers.

@nxrighthere
Copy link

Uh, it would be great if we do some cleanup/closing/tagging with such proposals to track the appropriate ones instead.

@jaredpar
Copy link
Member

@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.

@nxrighthere
Copy link

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.

@jvbsl
Copy link

jvbsl commented May 18, 2020

Then I was upset for nothing :D Thanks for the clarification

@NetMage
Copy link

NetMage commented Sep 15, 2020

Shouldn't the initial post be updated as Finalized and perhaps a line added pointing to the superceding feature as the reason?

@jaredpar
Copy link
Member

Once we ship yes.

@333fred
Copy link
Member

333fred commented Feb 18, 2021

@333fred 333fred closed this as completed Feb 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests