-
Notifications
You must be signed in to change notification settings - Fork 95
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
Add [Foreign.dynamic_funptr] #595
Conversation
[Foreign.dynamic_funptr] is a safer alternative for [Foreign.funptr], in particular this version uses explicit life cycle management making it easier to avoid dificult to debug segmentation faults where the ocaml closure is released before it is used from C.
Thanks for the proposal! |
The OCaml type systems only enforces the types on the OCaml side, not the corresponding types at the C side, that are also part of the contract. It's not a problem with the current interface, because you define the function and its callbacks together. With your new interface there can be a mismatch that is not longer captured by the type system, e.g with: let foo = foreign "foo" (int @-> dynamic_funptr (int @-> int @-> returning int) @-> returning void) I could allocate the corresponding dnymaic_funptr for let dynptr1 = dynamic_funptr_of_fun (int8_t @-> int8_t @-> returning int8_t) (+)
let dynptr2 = dynamic_funptr_of_fun (camlint @-> camlint @-> returning camlint) (+)
let no_int_at_all = Ctypes.view ~read:(fun _ -> 1) ~write:(fun _ -> "aaa") Ctypes.string
let dynptr3 = dynamic_funptr_of_fun (no_int_at_all @-> no_int_at_all @-> returning no_int_at_all) (+) |
Tanks for flagging this! We have a couple of ideas to fix this, I'll follow up once we have this fleshed out a bit more. |
I have just pushed what I think should fix this nicely, here's a slightly contrived example of how to use this
The the |
In particular this renames `module type Dynamic_funptr` to `module type Funptr` and drops a redundant debug arg.
FWIW I have been using this new API to bind to C with maybe 50 callbacks. I basically allow them to leak because it is test code - but otherwise, the project would not have been possible because of the crashes that the GC would (and did) cause. I am not sure I like the way the new functions integrate with the old ones - some distinction between safe and "raw" would be nice I think. |
Thanks again for this PR, and apologies for my long delay in responding. As I said above, I think adding explicit allocation/deallocation for function pointers is a good direction. Looking at the code, I see several other things I like: using roots, catching double frees, and catching memory leaks by attaching a finaliser. One thing I'm not so convinced about is the proposed interface, so I'm going to suggest an alternative that seems simpler to me. If I'm not mistaken, we can do everything we need with a pair of functions that construct and deallocate val make_funptr : ('a -> 'b) static_funptr typ -> ('a -> 'b) -> ('a -> 'b) static_funptr
val release_funptr : ('a -> 'b) static_funptr -> unit For example, your let progress_callback_t = static_funptr (int @-> int @-> ptr void @-> returning void)
let keygen = foreign "RSA_generate_key"
(int @-> int @-> progress_callback_t @-> ptr void @-> returning rsa_key)
let secret_key =
let progress = make_funptr progress_callback_t
(fun a b _ -> printf "progress: a:%d, b:%d\n" a b) in
let key = keygen 2048 65537 progress null in
let () = release_funptr progress in
key (If there's something that you think can't be expressed with this interface, please do say!) Then the semantics follow what you have already: the valid pattern usage pattern is
Everything else is invalid, including
Regarding the implementation, I was going to suggest extending I hope to merge this change when everything is addressed, so I'm going to mention a couple more things now. First, the change needs some tests --- ideally, tests that exercise the various failure cases (use-after-free, double-free, leak, etc.). Second, the documentation is currently not to my taste: it's too implementation-specific (e.g. talking about segmentation faults), too opinionated and sometimes too vague. I'm happy to make some more concrete suggestions for documentation once we have consensus on the interface. |
Thanks for the feedback! I originally attempted to do something similar to what you're suggesting but this had soundness issues that @fdopen pointed out. We could get something similar to your proposal by changing the interface to use a phantom type (similar to [Core.Map]), but I think we still need to retain some type witness to ensure that the ctype of the funptr agrees with the argument its being used in. Something like the following maybe? type ('ocaml_type, 'witness) safe_funptr
val make_funptr : ('ocaml_type, 'witness) safe_funptr typ -> 'ocaml_type -> ('ocaml_type,'witness) safe_funptr
val release_funptr : ('ocaml_type,'witness) safe_funptr -> unit
module Progress_callback = Foreign.Make_funptr(val (
Foreign.safe_funptr (int @-> int @-> ptr void @-> returning void))
let keygen =
foreign "RSA_generate_key" (int @-> int @-> Progress_callback.t @-> ptr void @-> returning rsa_key)
let secret_key =
let progress = make_funptr progress_callback_t
(fun a b _ -> printf "progress: a:%d, b:%d\n" a b) in
let key = keygen 2048 65537 progress null in
let () = release_funptr progress in
key I don't think its possible to avoid the use of a functor to get a unique (witness) type. The nesting of first class module and functor while a bit obscure is a trick to get the compiler to infer all the necessary types from the ctype without having to spell out everything. It is also possible to create something more like to old interface where the FFI closure is dynamically allocated, but with a more explicit free-step. This would allow an interface very similar to your proposal and also address the safety issues when dealing with closures from ocaml, however there remains some non-obvious issues when dealing with funptrs returned from C. Please let me know which direction you think is a better fit for ctypes? Re documentation & testing - point taken |
Anyone know what this is stuck on? This is current blocking our next stable release, all it would be lovely to get it unstuck... |
Thanks for the prompt, and apologies (again!) for the slow response. I'd also like to have this resolved soon, and will follow up properly later today. |
Okay, I've read through the commit history and I see @fdopen's point about type safety and your neat functor-based fix for the problem he points out. I'm happy to have the extra type safety of the functor approach, but I'd like to keep the interface as simple as possible. Do you think the following interface (which removes module type Funptr = sig (* what you have already *) end
val static_funptr : ?abi:Libffi_abi.abi -> ?runtime_lock:bool -> ?thread_registration:bool -> ('a -> 'b) Ctypes.fn ->
(module Funptr with type fn = 'a -> 'b) For example, with this interface your module Progress_callback = (val static_funptr (int @-> int @-> ptr void @-> returning void))
let keygen =
foreign "RSA_generate_key" (int @-> int @-> Progress_callback.t @-> ptr void @-> returning rsa_key)
let secret_key =
Progress_callback.with_fun
(fun a b _ -> Printf.printf "progress: a:%d, b:%d\n" a b)
(fun progress -> keygen 2048 65537 progress null) |
Unfortunately I have no objections to adding |
Could you expand on this a bit? At the moment, I'm interested in exploring the space of solutions, rather than fixing on a particular approach. Perhaps a concrete example would help, if you have one in mind. |
Here's the specific use case where module Bindings (F : Cstubs.FOREIGN) = struct
(* ... *)
module Progress_callback = Foreign.Make_funptr(
(val Foreign.funptr_spec Ctypes_static.(int @-> int @-> ptr void @-> returning void)))
(* ... *)
let generate_parameters = foreign "DH_generate_parameters"
Ctypes.(int @-> int @-> Progress_callback.t_opt @-> ptr void @-> returning Dh.t)
(* ... *)
end This The workarounds I can see are
|
Thanks for the example. It feels that there's something not quite right here, somehow. I don't see how we can have something that simultaneously:
Doesn't your proposed interface still have @fdopen's type safety problem? Suppose we have two module Spec1 = (val Foreign.funptr_spec Ctypes_static.(int @-> int @-> ptr void @-> returning void))
module Spec2 = (val Foreign.funptr_spec Ctypes_static.(int8_t @-> int8_t @-> ptr void @-> returning void)) Then we might define a functor which builds another let flag = ref false
module F(S: sig end) = struct include Spec1 let fn = if !flag then Spec1.fn else Spec2.fn end Now calling module U = struct end
let () = flag := false
module Progress_callback1 = Foreign.Make_funptr(F(U))
let () = flag := true
module Progress_callback2 = Foreign.Make_funptr(F(U)) and so we can build a function let f = Foreign.foreign "foo" Ctypes.(Progress_callback1.t @-> returning void)
f (Progress_callback2.of_fun (fun _ _ _ -> ()))
This seems like a fairly reasonable approach. Is moving the type definition away outside of the functor really such a problem? It doesn't seem to depend on the functor argument, so in some respects moving it outside the functor makes the code structure clearer.
Yes, I'd also like to avoid this. (Perhaps one day OCaml will have generativity polymorphism and we can update the cstubs generator to support generative functors in a backwards-compatible way.) |
… `(val dynamic_funptr ..)`.
Thanks for the feedback, I agree that writing a custom Making the changes to split out the type definition has proven to be less messy then I expected so lets go with the simpler API, I have pushed the changes. I think |
It's good to see that we've reached consensus on the interface. I'll aim to provide a proper code review in the next few days. |
@yallop ping |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also add some tests, including
- using
Funptr.t
values with the dynamic (libffi) interface - using
Funptr.t
values with the static (cstubs) interface - using
Funptr.t
values in a struct or array - lifetimes that extend beyond a single call, e.g.:
(a) allocate a function pointer that toggles abool ref
in OCaml
(b) pass the function pointer to a C function, which stores it and returns
(c) callGc.compact
(perhaps twice)
(d) check from OCaml that the OCaml closure is still alive (e.g. using another flag set in the finalizer)
(e) call a second C function which invokes the stored function pointer
(f) check that the call took place by examining thebool ref
- auxiliary function such as
with_fun
,t_opt
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the feedback!
I've updated the API and documentation as suggested.
I'm still working on the tests
I've added unit tests that should give basic coverage of everything. |
* use 'fun (type a) (type b) ...' instead of 'fun (type a b)' * use Gc.finalise instead of Gc.finalise_last
af2b1d1
to
760f638
Compare
I've pushed one more commit (760f638) to prevent flambda from turning the closure The 32-bit Windows build is still failing, but that seems to be unrelated to this PR, so I've opened a separate issue in the |
Thanks again for all your work on this very useful addition, @tiash. (And thanks, too, to @fdopen, @andrewray and @avsm for comments and review.) This is now merged in |
Many thanks @yallop for taking the time to review this and for all the feedback you provided to improve the pull request! |
[Foreign.dynamic_funptr] is a safer alternative for [Foreign.funptr], in
particular this new version requires explicit life cycle management making it
easier to avoid difficult to debug segmentation faults where the ocaml
closure is released before it is used from C.
We discovered one of these in Async_ssl which motivated us to propose this addition.