bmitc
bmitc

Reputation: 843

Creating functions that take in generic delegates in F#

I have created some F# bindings for GLFW. For this, there are several callbacks. Right now, all of my set callback wrappers are basically identical aside from the callback definition (which is a delegate type), a more idiomatic F# callback function, the arguments of these function types, and the actual binding that marshals the callback function to GLFW.

Here are two bindings and callback delegates:

module Windowing.GLFW.Native.Window
open System.Runtime.InteropServices

[<Struct>]
type GLFWwindow = struct end

type GLFWwindowsizefun = delegate of window: nativeptr<GLFWwindow> * width_height: (int * int) -> unit

type GLFWwindowfocusfun = delegate of window: nativeptr<GLFWwindow> * focused: int -> unit

[<DllImport(glfwDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern GLFWwindowsizefun glfwSetWindowSizeCallback(GLFWwindow* window, GLFWwindowsizefun callback)

/// Sets the focus callback for the specified window.
[<DllImport(glfwDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern GLFWwindowfocusfun glfwSetWindowFocusCallback(GLFWwindow* window, GLFWwindowfocusfun callback

Then I have high-level wrappers that are meant to hide the interop details:

module Windowing.GLFW.FSharp
open Windowing.GLFW.Native
open FSharp.NativeInterop
open System.Runtime.InteropServices

type GLFWWindow = GLFWWindow of nativeptr<Window.GLFWwindow>

let setWindowSizeCallback (callbackRef: outref<Window.GLFWwindowsizefun>)
                          (callback: GLFWWindow * int * int -> unit)
                          (GLFWWindow window) =
    let callback = Window.GLFWwindowsizefun(fun window (width, height) -> callback(GLFWWindow window, width, height))
    callbackRef <- callback
    Window.glfwSetWindowSizeCallback(window, callback) |> ignore

let setWindowFocusCallback (callbackRef: outref<Window.GLFWwindowfocusfun>)
                           (callback: GLFWWindow * int -> unit)
                           (GLFWWindow window) =
    let callback = Window.GLFWwindowfocusfun(fun window focused -> callback(GLFWWindow window, focused))
    callbackRef <- callback
    Window.glfwSetWindowFocusCallback(window, callback) |> ignore

As you can see, these wrapper functions are practically identical. To create a new one, I just copy one, change the function delegate types, the callback type definition, and the GLFW binding.

What I would like is to make a generic function that handles all of these mechanisms, and then to define the actual wrappers, I just pass in the relevant type values. However, I have been unable to get this to work with generic delegate constraints.

For example, I have tried this:

let setGenericCallback (callbackDef: 'GLFWcallback when 'GLFWcallback: delegate<nativeptr<Window.GLFWwindow> * 'Args, unit>)
                       (setCallbackFun: nativeptr<Window.GLFWwindow> * 'GLFWcallback -> 'GLFWcallback)
                       (callback: GLFWWindow * 'Args -> unit)
                       (callbackRef: outref<'GLFWcallback>)
                       (GLFWWindow window) =
    let callback = callbackDef(fun window args -> callback(GLFWWindow window, args))
    callbackRef <- callback
    setCallbackFun(window, callback) |> ignore

But on the line let callback = callbackDef ..., I get the error "This value is not a function and cannot be applied". If I try callbackDef.Invoke, I get the error "Lookup on object of indeterminate type", even though it's supposed to be a delegate. What is also weird is that I am not able to statically constraint 'GLFWcallback with ^GLFWcallback. It appears that outref<type> cannot take a statically constrainted type for type.

The two resources I have been referring to are:

If I had such a generic function, then I could redefine things as:

let setWindowSizeCallback callbackRef callback window =
    setGenericCallback Window.GLFWwindowposfun
                       Window.glfwSetWindowPosCallback
                       callback
                       callbackRef
                       window

So, I am stuck. The errors I am getting don't make sense to me or tell me what's really wrong. It doesn't seem like the delegate constraint is working properly or that I'm using it properly.

How do I get this working? How do I properly define a function like this that allows me to use generic delegates? Note that my delegates are not all that generic. They are all of the type delegate of nativeptr<GLFWwindow> * 'T -> unit.

Upvotes: 3

Views: 171

Answers (2)

linkhyrule5
linkhyrule5

Reputation: 916

After running into the same problem, further research eventually turned up this question and the following bit of spec:

5.2.8 Delegate Constraints An explicit delegate constraint has the following form:

typar : delegate<tupled-arg-type, return-type>

During constraint solving (§14.5), the constraint type : delegate<tupled-arg-type, return-types> is met if type is a delegate type D with declaration type D = delegate of object * arg1 * ... * argN and tupled-arg-type = arg1 * ... * argN. That is, the delegate must match the CLI design pattern where the sender object is the first argument to the event.

Note: This constraint form exists primarily to allow the definition of certain F# library functions that are related to event programming. It is rarely used directly in F# programming.

The delegate constraint does not imply anything about subtypes. In particular, a ‘delegate’ constraint does not imply that the type is a subtype of System.Delegate.

The delegate constraint applies only to delegate types that follow the usual form for CLI event handlers, where the first argument is a “sender” object. The reason is that the purpose of the constraint is to simplify the presentation of CLI event handlers to the F# programmer.

So basically, the delegate constraint is only tangentially related to the actual problem of constraining types to be delegates, and you'd be better off defining an interface like

type IInvokable<'a, 'b> = interface
    abstract member Invoke: 'a -> 'b
end

and then constraining to :> IInvokable.

Upvotes: 0

Tomas Petricek
Tomas Petricek

Reputation: 243051

You may be able to cast the typed delegate into an untyped delegate and then invoke it dynamically. I'm also not sure what the performance implications are - I suspect it may be slower, but I have not tried this.

I was not able to run this, but the following type-checks (but I made some edits as I was confused by the difference between GLFWindow and GLFwindow):

let inline setGenericCallback 
  (callbackDef: 'GLFWcallback when 'GLFWcallback: delegate<nativeptr<GLFWwindow> * 'Args, unit>) 
      (callback: GLFWWindow * (int * int) -> unit) (GLFWWindow window) =
    let callback = 
      (callbackDef :> System.Delegate).DynamicInvoke
        [| fun window args -> callback(GLFWWindow window, args) |]
    ()

That said, I agree with what other commenters said - it is just easier to live with some code duplication. This makes especially sense in code that is basically manually auto-generated binding.

Upvotes: 1

Related Questions