Search code examples
cygwinocamlopamlibffiutop

Error trying to `#require "ctypes.foreign";;` in utop on Windows (Cygwin)


I managed to install and run OCaml + OPAM + utop on Windows (in Cygwin) thanks to the awesome guide by Jonathan Protzenko. In utop, I especially want to be able to use ctypes + ctypes.foreign to quickly experiment and prototype accessing various WinAPI calls. Theoretically, I managed to install the ctypes packages successfully (opam install ... succeeds). Unfortunately, when I try to actually load them in utop, they fail miserably with a surprising error message:

utop # #require "ctypes.foreign";;
Error: Reference to undefined global `Ctypes_closure_properties'

Trying to do the same in rlwrap ocaml gives a somewhat longer error message:

# #require "ctypes.foreign";;
C:\OCaml\lib\unix.cma: loaded
C:\OCaml\lib\bigarray.cma: loaded
C:\OCaml\lib\str.cma: loaded
C:\cygwin64\home\Mateusz\.opam\system\lib\bytes: added to search path
C:\cygwin64\home\Mateusz\.opam\system\lib\ctypes: added to search path
C:\cygwin64\home\Mateusz\.opam\system\lib\ctypes\ctypes.cma: loaded
C:\cygwin64\home\Mateusz\.opam\system\lib\ctypes\ctypes-foreign-base.cma: loaded
Cannot load required shared library dllctypes-foreign-base_stubs.
Reason: dllctypes-foreign-base_stubs.dll: Cannot resolve ffi_type_pointer.
C:\cygwin64\home\Mateusz\.opam\system\lib\ctypes\ctypes-foreign-unthreaded.cma: loaded
Characters -1--1:
  #require "ctypes.foreign";;

Error: Reference to undefined global `Ctypes_closure_properties'

Note: full transcript of the sessions is here — it contains output of a utop session in the cmd.exe terminal, and also of an additional rlwrap ocaml session in the Cygwin terminal.

I have absolutely no idea why that happens, or how I could further try to debug/pinpoint/diagnose the situation, so that I could try to find some workaround. (And by the way — I'm a total newbie to OCaml & OPAM, although I'm reasonably experienced in C/C++ and Linux generally.) What follows are some questions I have, that I imagine might hopefully help nudge the problem enough to push me through the roadblock:

  • Is there a way to tweak a git repo (i.e. ctypes) locally and pass it to OPAM instead of the original github one?
  • I've managed to google up some comment, which seems to mention that "a static version of libffi" might work, as hinted also in ctypes issue #198; but I'm a total newbie to OCaml, and unfortunately I have no faintest idea how I could apply this suggestion to utop?
  • How can I tell OPAM which libffi to use for ctypes? (specifically, to use "static libffi"?)
  • How can I check if the mingw64-x86_64-libffi Cygwin package I do have installed is statically linked, or dynamically? or if both variants are provided, then which one is used by ctypes? I found some answers on SO regarding how to detect if a libffi.a is linked with PIC (?) relocations [-fpic?], but they all seemed really hackish and none of the ones I tried seemed really to work, at least in my case.
  • Any at all help would be highly appreciated. I don't even know who to ask about this, given it's at a crossing of OCaml, OPAM, utop and Cygwin, all of those being huge and somewhat unrelated/orthogonal systems. I've tried posting the issue on ctypes repo, but all it got over a month's time was a "patches-welcome" label.

Solution

  • mingw64-x86_64-libffi ships with static and dynamic libraries, but the gnu linker will use the dynamic versions by default.

    Patching the build of ctypes would be very fiddling (the build system of ctypes is rather complex, a mix of scripts and makefiles). So just try the following: delete the dynamic libraries (/usr/x86_64-w64-mingw32/sys-root/mingw/lib/libffi.dll.a and /usr/x86_64-w64-mingw32/sys-root/mingw/bin/libffi-6.dll), rebuild ctypes and then restore the deleted files.

    opam-repository-mingw

    By the way: https://fdopen.github.io/opam-repository-mingw/installation/ contains a patched version of flexdll (https://github.com/alainfrisch/flexdll/pull/3 applied - the problem is not specific to libffi) that supports the dynamic and static versions of libffi. opam install ctypes-foreign ctypes utop should work out of the box, if /usr/x86_64-w64-mingw32/sys-root/mingw/bin is in your PATH. Full step-by-step guide below:

    1. Download a 64-bit graphical installer and run it.
    2. After installation completes, start the "Cygwin64 Terminal" (gets installed into Cygwin group in the Start Menu).
    3. Type cygwin-install gui to start the Cygwin Setup/Installer and use it to install some text editor (joe or nano or vim or whatever you prefer).
    4. Use the text editor you installed to edit your ~/.bashrc file, add the following line:

      export PATH="/usr/x86_64-w64-mingw32/sys-root/mingw/bin:$PATH"
      

      then execute it in your Cygwin Terminal (just type it in the terminal, or run the following command instead):

      $ source ~/.bashrc
      
    5. Install depext which will auto-download any required OS level (native) dependencies for OPAM packages, the use it to install ctypes and utop:

      $ opam install depext depext-cygwinports
      $ opam depext -i ctypes-foreign ctypes utop
      
    6. Switch from Cygwin Terminal to cmd.exe, for example by typing the following command in Cygwin Terminal:

      $ cmd /c start cmd
      
    7. Start utop in cmd.exe terminal, and use it to call the MessageBox WinAPI function as a test that all's working well:

      c:> utop
      
      utop # #require "ctypes.foreign";;
      utop # let dll = Dl.dlopen ~filename:"user32.dll" ~flags:[];;
      val dll : Dl.library = <abstr>
      utop # open Ctypes;;
      utop # let mb =
        Foreign.foreign ~from:dll "MessageBoxA"
        (ptr void @-> string @-> string @-> uint @-> returning int) ;;
      val mb : unit Ctypes_static.ptr -> bytes -> bytes -> Unsigned.uint -> int =
        <fun>
      utop # mb null "hello" "world" Unsigned.UInt.zero;;
      - : int = 1
      utop # #quit;;
      
      c:>
      

      [NOTE: not all of the above utop commands may be required, but that's what worked for me.]

      utop + ctypes.foreign + WinAPI = MessageBox