[ANN] Camlkit -- macOS/iOS/GNUstep toolkit for OCaml

I am pleased to announce the alpha release of Camlkit.
Camlkit provides OCaml bindings to a collection of Cocoa frameworks on macOS, iOS, and GNUstep. Currently available are Foundation, AppKit, UIKit, WebKit, CoreImage, Photos, and Vision.

The package camlkit for macOS/GNUstep development is available from OPAM. The key libraries to add to your dune file are camlkit-base.foundation and camlkit-gui.appkit.

iOS development requires a cross-toolchain. The package is named camlkit-ios. UIKit is in the library camlkit-gui.uikit.

Other useful resources:

More information is available on the project’s github page. Feedback and contributions are welcome. I hope we can make OCaml viable for GUI development and on mobile. Happy hacking!

25 Likes

This is awesome – I’ve wanted something like this for a long time, thanks for making the effort! Curious if you’re using it to build anything in particular?

No, nothing in particular. The project has not proven itself on a large-scale codebase. Just toy examples for the moment.

Wow, that is an awesome project. I wonder how easily it can be integrated with an Ocsigen project for the backend.

The README says:

This OPAM repository contains an iOS toolchain featuring OCaml 4.14.2 and 5.0.0 and some commonly used packages.

Is there a problem with supporting 5.1 / 5.2? They should have improved GC behaviour which would be helpful on iOS devices.

32-bit iOS device cross-compiling is only supported in OCaml 4.04.0.

Do you know why this is? I thought 4.14 supported 32bit devices and would be suitable for targeting armv7s. Are 32bit iOS devices still common enough to want to target?

The OCaml cross-compiler in opam-cross-ios is based on a patchset by Gerd Stolpmann.

This link to the patchset is missing, do you know where I might find it? Curious to see what could be done to upstream that. This seems like a perfect example of cross compiling to support, along with MirageOS cross compiling.

Is there a problem with supporting 5.1 / 5.2?

Yes, I tried it and hit a roadblock but wasn’t motivated enough to follow the rabbit hole. The OCaml build system is not getting more amenable for cross-compilation with every new release, unfortunately.

Do you know why this is? I thought 4.14 supported 32bit devices and would be suitable for targeting armv7s. Are 32bit iOS devices still common enough to want to target?

The last toolchain that likely supports 32-bit in opam-cross-ios is 4.07.1, courtesy of markghayden. I haven’t tested it. 32bit iOS devices have long been obsolete, but on the other hand those millions of obsolete devices are perfect for tinkering, so there is some value to support them.

This link to the patchset is missing, do you know where I might find it?

See this thread.

1 Like

I wonder how easily it can be integrated with an Ocsigen project for the backend

If the Ocsigen project is used as a classic REST/RPC api backend, integration should not be difficult. On the other hand, if we are talking about a multi-tier application where the client and backend are part of a single program, that would probably be challenging. I would also like to know what would be involved. It would be awesome if we can achieve this.

What I was thinking is to have Camlkit as the mobile application UI bindings alongside Ocsigen-toolkit and the Cordova bindings, like what is described on Mobile applications with Ocsigen. I expect the tricky part will be describing the cross compile part to dune so it will emit an iOS binary alongside the server binary in arm64/x86_64.

1 Like

Better than 4.14 though? Isn’t 4.14 still the latest version with auto-compaction?

1 Like

This is a fantastic library. Thank you for providing it!

I’m curious about how you generate all these “auto-generated” files. Is the code generator available somewhere? What’s the process of adding new library support? For instance, if I’m interested in adding FSEvent support, should I do this in a separate library (for instance, in ocaml-fsevents), or would you prefer a PR to camlkit? In both cases, is there any piece of machinery and/or tips you are willing to share on how to do this properly?

Thanks!

I published the bindings generator project. It is not documented but here’s a short guide. PRs for library bindings or improving the generator are welcome.

Objective: Generate bindings for the FSEvents API

The API of interest, according to Apple’s documentation, is part of the Core Services framework. It is supported on all platforms, including macOS, so generating bindings for it should be easy.

  1. Find the “bridgesupport” file

/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Resources/BridgeSupport/FSEvents.bridgesupport

  1. Generate globals
mkdir -p data/FSEvents
cp /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Resources/BridgeSupport/FSEvents.bridgesupport data
dune exec bs-to-ml -- -fw FSEvents < data/FSEvents.bridgesupport > data/FSEvents/FSEvents_globals.ml

This covers the C API that cannot be introspected via the Objective-C runtime. Let’s check if the framework contains Objective-C classes.

dune exec inspect-rt -- -libs | grep FSEvents

Doesn’t look like it does. We are done. Otherwise, we would proceed with the next tool:

  1. Generate bindings to Objective-C classes
cd data/FSEvents && dune exec generate-ml -- -fw FSEvents -classes <FSEvents framework binary>

Libraries that are iOS-only do not provide bridgesupport files. For those, you have to jump through many hoops. Eg, you can attempt to generate a bridgesupport file manually:

gen_bridge_metadata --arm64e -C -isysroot=$(SDK_IOS) -f $(SDK_IOS)/System/Library/Frameworks/<Whatever>.framework -o data/<Whatever>.bridgesupport

That’s about it. Have fun.

5 Likes

What’s an example of this argument? Is it a path, a name, or? If it’s a path, how do I find it?

I’m trying to do this for CoreBluetooth, and I think I got somewhere (I have globals generated), but I stopped at generate-ml

This is actually the “name of the dynamic library”, most of the time corresponding to the filesystem path of the library binary. In the Objective-C runtime, it is referred to as the image name. If you know the name of a class that is part of that library, you can obtain the image name like so:

dune exec inspect-rt -- -image CBService -load /System/Library/Frameworks/CoreBluetooth.framework
/System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth

Ah, OK. So the output of that command can be used as the input to -classes?

I just tried that (inspect-rt outputs the same image name as in your example) - what should I expect to happen? The command doesn’t say anything, but I’m not sure what it should be outputting :slight_smile:

I forgot to mention that for non-common frameworks, you have to load the framework explicitly in order to register the classes with the Objective-C runtime. This is done with the load argument:

cd data/CoreBluetooth && dune exec generate-ml -- -classes /System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth \
  -fw CoreBluetooth \
  -load /System/Library/Frameworks/CoreBluetooth.framework

Then look for the generated bindings in data/CoreBluetooth.

When you add a dune file and try to compile the generated bindings, you will discover what other libraries the framework depends on. For example, this framework uses CFUUID, which is part of CoreFoundation. You can repeat the bindings generation step adding an additional parameter open, whose value is the comma-separated list of library dependencies:

cd data/CoreBluetooth && dune exec generate-ml -- -classes /System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth \
  -fw CoreBluetooth \
  -load /System/Library/Frameworks/CoreBluetooth.framework \
  -open CoreFoundation

Ah, when loading, I see all the classes generated now, nice!

I see, when you pass a -load param, the generated code gets this added, for example:

[@@@ocaml.warning "-33"]
open CoreFoundation

OK, I haven’t tried using it yet, but it seems to compile.

However, I commented out open Objc in CoreBluetooth_globals.ml since it’s unused and dune was complaining.

I browsed camlkit and found that, e.g., SpriteKit also doesn’t open Objc, so I assume there’s nothing wrong with how the globals were generated. Probably could tag it with [@@@ocaml.warning "-33"], too

Yes, good point. I was too lazy to add it because I only encountered this case with SpriteKit.

1 Like

just stumbled across this and it looks interesting! how good/complete is the gnustep support, and would you say this toolkit is worth exploring if my main interest is developing cross-platform desktop apps?

At the level of the Objective-C runtime, I think gnustep support is pretty good. I’ve taken into account the api differencies, and I’m not aware of remaining issues.

At the level of the framework bindings, gnustep support is not as good. I am generating the bindings based on the macOS implementations. To the extent that gnustep maintains compatibility with Apple’s frameworks (which is their objective), you are likely to encounter only few issues. I remember struggling with code that relied on NSProxy, where the api is compatible, but behaviour seems to differ between the platforms. On the other hand, there are api differencies at this level that you’ll have to account for. Some classes/methods implemented in the Apple’s frameworks are not implemented in gnustep’s.

The biggest issue though is the poor support of the gnustep libraries in the Linux ecosystem. Unlike FreeBSD, virtually all Linux distributions package outdated gnustep libraries built on top of the ancient Objective-C runtime which is distributed with gcc, and which has a totally incompatible api. Until this state of affairs improves, my conclusion is that gnustep is not a viable option for the development of cross-platform desktop applications.

1 Like

OK, I’m back to it…

I’ll probably have more questions - is there a better place to ask them?

My latest one is how to make the behavior in camlkit of this code match what I see in Objective-C:

let () =
  let open Runtime in
  let open Objc in
  let proto = Objc.get_protocol "CBCentralManagerDelegate" in
  if not (is_nil proto) then
    Printf.printf "Protocol exists\n"
  else
    Printf.printf "Protocol does not exist\n";

Prints “Protocol does not exist”

But this:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    Protocol *myProtocol = objc_getProtocol("CBCentralManagerDelegate");

    if (myProtocol) {
        NSLog(@"Protocol exists");
    } else {
        NSLog(@"Protocol does not exist");
    }
}

prints “Protocol exists”.

What’s missing?