3

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.

1
  • 1
    I tinkered with this for a little while without success. My guess is that between the restrictions of byrefs, delegates, and P/Invoke there isn't an elegant solution. You will be better off just duplicating those few lines. Commented Jul 3, 2022 at 9:41

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for your help on this! This did indeed typecheck in isolation (need to replace (int * int) with 'Args though), but it didn't typecheck in the broader context. I can't remember off of the top of my head what the issue was, but I ended up reverting back to just creating all the functions by copying over code. It's a fair amount of duplicated code since there are dozens of callbacks, but it works and is fairly static (unless I change the mechanism by which the callback references are handled). I can circle back and post here what the issue was for completeness.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.