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

Function pointers #934

Open
4 of 5 tasks
DalekBaldwin opened this issue Nov 17, 2020 · 29 comments
Open
4 of 5 tasks

Function pointers #934

DalekBaldwin opened this issue Nov 17, 2020 · 29 comments

Comments

@DalekBaldwin
Copy link

DalekBaldwin commented Nov 17, 2020

Function pointers

I propose we offer mechanisms to create, consume, and invoke function pointers, along the lines of the new constructs available in C# 9.0: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/function-pointers.md

The existing way of approaching this problem in F# is nonexistent. It may be possible to write a separate C# assembly when these features are needed and call methods in that assembly from F#, but in most cases this would probably introduce additional burdens of benchmarking and IL code inspection to verify whether the cost of this indirection negates the benefit of faster invocation of the functions referenced via function pointers in the C# code.

Pros and Cons

The advantages of making this adjustment to F# are expanding the opportunities for writing comprehensible and maintainable code that generates high-performance functionality at runtime without resorting to here-be-dragons features of .NET like ILGenerator. F# arguably offers the most pleasant experience in the .NET language ecosystem for this style of development, so it would be nice to have feature parity with C# in writing code that compiles to faster IL opcodes.

The disadvantages of making this adjustment to F# are exposing new potentially unsafe constructs in a language that doesn't have unsafe blocks like in C#. But such constructs already exist and it is considered the responsibility of programmers and library users to be aware of the implications of their use.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M -- although this may be a tricky new feature for F#, the C# compiler may provide a starting point for a syntax proposal, a reference implementation, and possibly some reusable code. It's not clear whether these features are fully finalized as of the C# 9.0 release, so we may want to wait until they are considered completely finished to take advantage of the C# team's code, experience, and documentation.

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@cartermp
Copy link
Member

I know that @TIHan has spent some time thinking about how to make these happen for F#. One of the big sticking points in C# was arriving at the syntax delegate*. We may have another fun problem to solve on that front. The semantics are unlikely to be terribly different from C# though, so that aspect of a design likely wouldn't change.

Also tagging as an interop area, though in this case it's more about native interop than C# interop (even if that's a scenario).

@Szer
Copy link

Szer commented Nov 23, 2020

NativePtr module
FunctionPtr module
:)

@Happypig375
Copy link
Contributor

This is needed for the DllImport source generator: dotnet/designs#181 (comment) F# won't be supported by it without this feature.

@7sharp9
Copy link
Member

7sharp9 commented May 25, 2021

This is interesting as usages have now filtered into real projects, for example the UnrealCLR plugin where they use function pointers so that there is no pinvoke overhead while using the unreal engine:

The interoperability in the plugin is not a traditional P/Invoke, which has a significant performance cost relative to regular function calls. That's why there is no traditional glue code. This approach passes runtime pointers - bypassing the symbol table entirely. As soon as the plugin is loaded by the engine into memory, initialization logic starts gathering pointers of native functions into internal buffers which are passed to the managed runtime

@7sharp9
Copy link
Member

7sharp9 commented May 25, 2021

This is needed for the DllImport source generator: dotnet/designs#181 (comment) F# won't be supported by it without this feature.

In F# we don't have Roslyn source generators anyway, so this is a moot point.

@realvictorprm
Copy link
Member

Would be great to have support for this too, would allow to do superb native interop with F# too without the requirement of C# code.

@Happypig375
Copy link
Contributor

We already have the delegate definition syntax:

type A = delegate of int byref * int inref * outref<int> -> unit

Why not transfer this syntax to function pointers?

C# F#
delegate*<void> nativeptr of unit -> unit
delegate*<int, void> nativeptr of int -> unit
delegate*<void, int> nativeptr of unit -> int
delegate*<int, int, int> nativeptr of int * int -> int
(Invalid) delegate*<int, void, int> (Invalid) nativeptr of int * unit -> int
delegate* managed<int, void, int> (Optional) [<Managed>] nativeptr of int * unit -> int/
nativeptr [<Managed>] of int * unit -> int
delegate* unmanaged<int, void, int> [<Unmanaged>] nativeptr of int * unit -> int/
nativeptr [<Unmanaged>] of int * unit -> int
delegate* unmanaged[Cdecl]<int, void, int> [<Cdecl>] nativeptr of int * unit -> int/
nativeptr [<Cdecl>] of int * unit -> int/
[<Unmanaged; Cdecl>] nativeptr of int * unit -> int/
nativeptr [<Unmanaged; Cdecl>] of int * unit -> int
delegate* unmanaged[Thiscall]<int, void, int> [<Thiscall>] nativeptr of int * unit -> int/
nativeptr [<Thiscall>] of int * unit -> int/
[<Unmanaged; Thiscall>] nativeptr of int * unit -> int/
nativeptr [<Unmanaged; Thiscall>] of int * unit -> int
delegate* unmanaged[Stdcall]<int, void, int> [<Stdcall>] nativeptr of int * unit -> int/
nativeptr [<Stdcall>] of int * unit -> int/
[<Unmanaged; Stdcall>] nativeptr of int * unit -> int/
nativeptr [<Unmanaged; Stdcall>] of int * unit -> int
delegate* unmanaged[Fastcall]<int, void, int> [<Fastcall>] nativeptr of int * unit -> int/
nativeptr [<Fastcall>] of int * unit -> int/
[<Unmanaged; Fastcall>] nativeptr of int * unit -> int/
nativeptr [<Unmanaged; Fastcall>] of int * unit -> int
delegate* unmanaged[Cdecl, SuppressGCTransition]<int, void, int> [<Cdecl; SuppressGCTransition>] nativeptr of int * unit -> int/
nativeptr [<Cdecl; SuppressGCTransition>] of int * unit -> int/
[<Unmanaged; Cdecl; SuppressGCTransition>] nativeptr of int * unit -> int/
nativeptr [<Unmanaged; Cdecl; SuppressGCTransition>] of int * unit -> int
&StaticMethod &&StaticMethod (Using the same operator as taking a nativeptr)

@Happypig375
Copy link
Contributor

Happypig375 commented Jun 18, 2021

As shown above, the positions of the pseudo-attributes still need a decision. They are not real attributes, rather they are looked up specially like with C#. Unlike C#, Unmanaged and (optionally) Managed can be specified there.

@Happypig375
Copy link
Contributor

However, I am unsure whether we should use the attribute syntax for pseudo-and-not-real-attributes.

@abelbraaksma
Copy link
Member

One use case for adding this would be DynamicMethod.CreateDelegate. I've been trying to find out a way that calling such a delegate with F# gives least overhead. Turns out this is far from trivial to do, even for advanced programmers.

Here's a little dump of some of the tests, here with C# (as that supports function pointers). Skip here means that CreateDelegate(type, types, true) is called, which skips scoping security checks. The IL is just a simple add a b = a + b, but as you can see, in high performance dynamic code, it is possible to outperform standard compiled, non-inlined members:

image

With F# I've gotten very close to this, I'll post my findings once I understand what's happening and why, but was never able to beat FnPtr, even with skip = true.

Technically, it should be possible for F# to write delegate function calls in a more efficient manner than it currently does, i.e. as fast as IL_Delegate_Instance_Skip (IL compiled to an instance method, called as a delegate). However, I've not been able to do that, in part because F# adds a level of indirection that isn't efficiently eliminated by the JIT.

The idea is that, with function pointers, F# could eliminate the indirection level and we'd gain a few percentages in high-speed dynamic computing (and perhaps also in other cases).

That, plus simply the ability to call function pointers from C#, or to emit calli for external calls, would be awesome. I'm not certain about using nativeptr instead of delegate*, as nativeptr is just an IntPtr for the compiler and using special syntax may help understand the code and may help the compiler making better decisions.

@Happypig375
Copy link
Contributor

@abelbraaksma There is still ilsigptr which is a real IL pointer. However, its use is unfamiliar to F# devs.

@abelbraaksma
Copy link
Member

@Happypig375 funny you say that, as I was just messaging you in that awesome work you did (#200), where you actually added ilsigptr to F#. With over 10yrs of F# experience, I can say: you're right, I'm unfamiliar with it.

@Happypig375
Copy link
Contributor

@abelbraaksma I only added functions to convert to and from ilsigptr. Meanwhile, the type itself has been there for at least 8 years, maybe even from F# 1.0.
https://github.com/dotnet/fsharp/blob/3900d411aeb2d7740f4e9fd72fce81b3a8f04357/src/fsharp/FSharp.Core/prim-types-prelude.fs#L42

@abelbraaksma
Copy link
Member

Which may be why it was so rarely used if you can't (trivially) convert in and out of it. Meanwhile, I was looking into fun libraries like this one (https://blog.devgenius.io/inline-assembly-in-f-net-language-6d70ab9f58c1). They are merely prototypes, in this case leveraging the Iced library for emitting x86 asm. In that post it is made clear that the lack of function pointers makes it rather hard to do, though he did find a hacky workaround (using this to get a Func from a function pointer).

Anyway, my experiments currently just evolved around emitting IL. I did find an optimized way to call this, but lack of calli support made it hard. Though I also found a (currently unsupported) way of calling the IL's dynamic method that is as fast as using calli.

@dsyme
Copy link
Collaborator

dsyme commented Apr 13, 2023

The need for the use of function pointers in F# programming is vanishingly rare and the implementation cost quite high.

I'll close this as "no" - however thank you for the suggestion and the discussion

@dsyme dsyme closed this as completed Apr 13, 2023
@smoothdeveloper
Copy link
Contributor

@dsyme, could you expand on "implementation cost quite high", giving pointers about implications on the IL code gen being more involved?

I think suggestions like this shouldn't be closed, but rather flagged as "Microsoft compiler team is not interested in implementing it", leaving open contribution from a motivated party.

I don't seem to grasp there is extraordinary work related to tooling integration for this feature, but mostly on the compiler frontend (syntax to support those declarations) and code generation (outputting the IL).

Maybe I'm missing something?

@smoothdeveloper
Copy link
Contributor

Also, it seems antithetical to https://twitter.com/dsymetweets/status/1224739090253467648 to relinquish this without giving pointers, and directing arrows that would dereference the implementation in the compiler.

@roboz0r
Copy link

roboz0r commented Apr 18, 2023

I did some experimentation on this topic today and, while I wasn't able to get it as fast as calling a function pointer in C#, I was able to make it roughly twice the speed of Marshal.GetDelegateForFunctionPointer for a dynamically loaded native dll. I guess the extra slowdown is due to the additional call to Invoke. Maybe a fully dynamic class and method would eliminate the overhead?

Seems like sF1 function pattern could be easily adapted to work with N parameters but if it's a dead-end wrt catching up with C# then I don't know much use it would be.

Method Mean Error StdDev Allocated
FnPtrCS 224.1 us 3.43 us 3.20 us -
FnPtrDelegate 1,057.9 us 20.57 us 26.01 us 1 B
FnPtrDynamicMethod 547.9 us 2.76 us 2.58 us 1 B

C# Dynamic Function Pointer

    unsafe public class FnPtrCS
    {
        private IntPtr dll;
        private delegate* unmanaged[Stdcall]<double,double> f2k;
        public FnPtrCS(string path)
        {
            dll = NativeLibrary.Load(path);
            f2k = (delegate* unmanaged[Stdcall]<double, double>)NativeLibrary.GetExport(dll, "F2K");
        }

        public double F2K(double T_F)
        {
            return f2k(T_F);
        }
    }

F# Dynamic Method:

    [<RequiresExplicitTypeArguments>]
    let private getDelegate<'TDelegate> dll name =
        let p = NativeLibrary.GetExport(dll, name)
        Marshal.GetDelegateForFunctionPointer<'TDelegate>(p)

    [<UnmanagedFunctionPointer(CallingConvention.StdCall)>]
    type private F2K = delegate of float -> float

    [<RequiresExplicitTypeArguments>]
    let sF1<'T0, 'TReturn> name callConv (fnPtr: nativeint) =
        let dyn = DynamicMethod(name, typeof<'TReturn>, [| typeof<'T0> |])
        let il = dyn.GetILGenerator()
        il.Emit(OpCodes.Ldarg_0)
        if Environment.Is64BitProcess then
            il.Emit(OpCodes.Ldc_I8, int64 fnPtr)
        else
            il.Emit(OpCodes.Ldc_I4, int fnPtr)

        il.EmitCalli(OpCodes.Calli, callConv, typeof<'TReturn>, [| typeof<'T0> |] )
        il.Emit(OpCodes.Ret)
        dyn.CreateDelegate<Func<'T0, 'TReturn>>()


    type FnPtrFS(pathToDll: string) =

        let dll = NativeLibrary.Load(pathToDll)
        let f2k = NativeLibrary.GetExport(dll, nameof F2K)
        let f2kIL = sF1<float,float> "f2kIL" CallingConvention.StdCall f2k
        let dF2K = getDelegate<F2K> dll (nameof F2K)

        member this.F2KIL(f: float) = 
            f2kIL.Invoke(f)

        member this.F2KDelegate(f: float) = 
            dF2K.Invoke(f)

Benchmarks:

[<MemoryDiagnoser>]
type FnPtrs() =
    let cs = new FnPtrCS(DllImport.DllName)
    let fs2 = new FnPtrFS(DllImport.DllName)

    [<Benchmark>]
    member __.FnPtrCS() = 
        let mutable x = 0.
        for i in 0 .. 100_000 do 
            x <- x + cs.F2K(i)
        x

    [<Benchmark>]
    member __.FnPtrDelegate() = 
        let mutable x = 0.
        for i in 0 .. 100_000 do 
            x <- x + fs2.F2KDelegate(i)
        x

    [<Benchmark>]
    member __.FnPtrDynamicMethod() = 
        let mutable x = 0.
        for i in 0 .. 100_000 do 
            x <- x + fs2.F2KIL(i)
        x

@dsyme
Copy link
Collaborator

dsyme commented Apr 23, 2023

@smoothdeveloper I'll take another look. However my gut feeling has always been that this particular emulation of C in F# is just too much - and also an unpleasant sinkhole for C# developers in C# trying to eek out that extra perf.

@dsyme dsyme reopened this Apr 23, 2023
@abelbraaksma
Copy link
Member

abelbraaksma commented May 19, 2024

@dsyme would you consider putting this on the roadmap (i.e., I'm willing to do the research & RFC if approved)?

While I sympathize with the idea that this is a relatively rare feature, it may find its uses in libraries (perf wise, or where delegates are dynamically created, like dynamic methods etc) and in CE design, where the intricacies of the function pointer itself are hidden.

Another argument that keeps cropping up from time to time is the ease of consuming code that exposes function pointers.

@dsyme
Copy link
Collaborator

dsyme commented May 20, 2024

@T-Gro @vzarytovskii Do you have thoughts here?

Personally I find the thought of putting such a primitive into F# really problematic - I just think it will lead far too many people to try to use this to eek out perf, when really they should be using other techniques (like selective inlining).

The problem is people would end up using it in the design of components - e.g. a new List.mapfp to take a function pointer. And this gets infectious all the way. And the actual need is probably lost in some larger picture. The end result will be a worse language, not a better one - one that's just emulating C. It's one thing to have NativePtr.* for data manipulation - but for function calling?

That said, there is still a consistent request for it above. Ultimately all that's needed to get the perf gain is some way to emit a calli instruction with an appropriate signature and adding . Maybe there should just be some way to do that, e.g. NativePtr.calli that is known to the compiler and has very special properties.

@vzarytovskii
Copy link

vzarytovskii commented May 20, 2024

@dsyme

@T-Gro @vzarytovskii Do you have thoughts here?

Personally I find the thought of putting such a primitive into F# really problematic - I just think it will lead far too many people to try to use this to eek out perf, when really they should be using other techniques (like selective inlining).

The problem is people would end up using it in the design of components - e.g. a new List.mapfp to take a function pointer. And this gets infectious all the way. And the actual need is probably lost in some larger picture. The end result will be a worse language, not a better one - one that's just emulating C. It's one thing to have NativePtr.* for data manipulation - but for function calling?

That said, there is still a consistent request for it above. Ultimately all that's needed to get the perf gain is some way to emit a calli instruction with an appropriate signature and adding . Maybe there should just be some way to do that, e.g. NativePtr.calli that is known to the compiler and has very special properties.

I am honestly conflicted about it, I understand the desire to have support for it - at least from the interop perspective, and from the perspective of minority of users trying to save all the "nanoseconds" they can.

I personally am not a huge fan of this feature - bringing such low level constructs to the language will inevitably lead to people overusing them and designing libraries around them.

But then again, I am slightly biased against it, since I have never had use cases for it.

@T-Gro
Copy link

T-Gro commented May 20, 2024

It would be good to get a set of real use cases and what it takes today to fullfill them, e.g. from the UnrealCLR engine or similar.
Even when the "what it takes" involves creating an intermediate C# project, I think it is worth keeping in mind that it is still an alternative.

Since the feature is inherently meant for unsafe contexts, I think it does not have place in F# programming using the full syntactical feature set it has in C#. But a few compiler-known functions in fslib could be the way to go to accomplish use cases which could be presented in this issue.

@roboz0r
Copy link

roboz0r commented May 20, 2024

If it helps put your minds at ease, my use case is entirely around native interop, in this case calling function pointers provided by Excel. I have no interest in rewriting core libraries to take function pointers.

My current workaround is to have a C# project that holds the function pointer in a struct and exposes it as a managed method to other internal consumers that provide a memory safe API to public consumers:

public unsafe readonly struct Excel12Proc
{
    readonly delegate* unmanaged[Stdcall]<int, int, nint, nint, int> _pfn;
    public Excel12Proc(nint pfn)
    {
        _pfn = (delegate* unmanaged[Stdcall]<int, int, nint, nint, int>)pfn;
    }
    public int Invoke(int xlfn, int count, nint ppOpers, nint pOperRes) => _pfn(xlfn, count, ppOpers, pOperRes);
}

This struct and the ability to consume the source generator "CsWin32" are the only uses of C# that I have in the solution. It's definitely not a dealbreaker, but it would be nice if these interop gaps could be filled by F# too. If the feature were in F#, it would also allow me to use typed pointers and avoid some casting at the call site:

        let code =
            excel12v.Invoke(
                xlFn,
                length,
                NativePtr.toNativeInt<nativeptr<xlOper12>> pFnParams,
                NativePtr.toNativeInt<xlOper12> &&resp
            )
            |> enum<xlRetCode>

@dsyme
Copy link
Collaborator

dsyme commented May 20, 2024

I'm wondering about a compiler-known intrinsic that can be used like this:

    NativePtr.calli<int * int * nativeint * nativeint, int, StdCall> _pfn (xlFn,
                length,
                NativePtr.toNativeInt<nativeptr<xlOper12>> pFnParams,
                NativePtr.toNativeInt<xlOper12> &&resp)

Not sure about the Unmanaged/Stdcall/Thiscall/Fastcall matrix - could be done via multiple helpers, or special types as type parameters.

module NativePtr =
    val inline calli<'Args, 'Ret, 'CallingConvention> : nativeint -> 'Args -> 'Ret

Here 'Args, 'Ret, 'CallingConvention would all need to be explicit, and should cover the excellent matrix given by @Happypig375 up above

@dsyme
Copy link
Collaborator

dsyme commented May 20, 2024

If something like that would do the job then yes, approve-in-principle.

@abelbraaksma
Copy link
Member

My personal use case is with an optimized dynamic library, where I found that using function pointers to native functions yielded a significant benefit when called from static contexts.

Consider a scenario that cannot be inlined (like a dynamic method). If the body is small, the overhead of the dynamic method call is significant compared to calli. For my specific case, I couldn't find a workaround, other than using C#.

I understand that this is a niche. I don't think anybody here suggests to use function pointers as overloads to F# Core lib functions. Introducing byref etc didn't lead to such proliferation either, right?

Thanks for looking into this again, @dsyme. I can put in the work for an RFC on the library functions, see if we can solve this without a language or compiler change.

@roboz0r
Copy link

roboz0r commented May 20, 2024

For my use I think that would definitely do the job.

It would be great to allow all unmanaged types in the function signature:

  • NativePtr.calli<int * int * nativeptr<nativeptr<xlOper12>> * nativeptr<xlOper12>, xlRetCode, StdCall>

Regarding CallingConvention there is an existing [<UnmanagedFunctionPointer>] but its use is restricted to delegates. It would at least provide a basis for what is available.

@DalekBaldwin
Copy link
Author

OP here: it's been a while, and I would now say that all the work that has gone into supporting static abstract members in interfaces provides about 80% of the intended benefits for 20% of the effort.

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