Automatically Packaging a Haskell Library as a Swift Binary XCFramework

I’m happy to announce I’ve released a library exporting Cabal SetupHooks which automatically produce an .xcframework (an Apple Binary Framework Bundle) from a Haskell library’s binary artifacts and foreign export headers at the end of cabal build.

The library is called xcframework and can be found on Hackage. Here’s the blog post introducing it:

18 Likes

This is very cool, I’ve had problems with integrating with macOS frameworks in the past because of Apple’s use of blocks (lambdas) not being supported by Haskell’s C tooling. I’d wanted to play with the endpoint security framework, but hit issues basically straight away. Have you run into any similar issues? Looking at your post, it feels like I was coming at the problem from the wrong direction, hoping to wrap the macOS headers using the C-FFI, but perhaps I should have been thinking about exposing Haskell functions.

2 Likes

I haven’t tried to go the other direction in calling Swift from Haskell, but I know of inline-c-objc by @chak which allows you to inline objc code into Haskell – it does require that the framework has objc bindings and I can imagine there are other problems beyond it. Anyway I thought it would be worth mentioning.

1 Like

Interesting, it’s a shame there’s no docs. The library I was looking to wrap was the endpoint security framework (Endpoint Security | Apple Developer Documentation) which caused problems with header files that were imported using Apple’s blocks extension to C, so none of the Haskell tooling could understand it, and I couldn’t wrap the structs. There’s probably a way to avoid using tools like c2hs or whatever I was trying, but it felt like a hell of a lot of busywork to just get to the point of being able to read and write structs.

Some of the core types in the library require using blocks too:

typedef void (^)(struct es_client_s *, const es_message_t *) es_handler_block_t;

Anyway, sorry for getting off topic, I feel like maybe I could build something in C that wraps Haskell and use your framework instead of trying to do things the other way around. Maybe I’ll see how far I can get with doing that sometime.

1 Like

Dude this is extremely sick. Nice work.

2 Likes

Nerd-sniped by @Axman6 's question, I took a look at how difficult it would be to make sure that hs-bindgen, the new tool that we are developing for automatic binding generation for C, would not choke on blocks. Turned out it wasn’t actually that difficult to actually add rudimentary but proper support for blocks. Given

typedef int(^VarCounter)(int increment);

we now generate (slightly cleaned up for readability)

newtype VarCounter = VarCounter (Block (CInt -> IO CInt))

Thanks for the inspriation :slight_smile:

-Edsko

PS. I recently became aware of a Rust package called hs-bindgen, for Rust-Haskell interop. We haven’t yet decided what to rename our tool too, but we likely will.

2 Likes

@edsko I’d always suspected it, but now I know, that you’re an angel sent from Lambda Heaven to answer my prayers.

I’ll have to give this a go tomorrow when I get some time, this was one of two seemingly insurmountable (without diving into c2hs’s source code at least) problems I was having in trying to wrap Apple’s ES framework. The other was the use of C++’s enum syntax (in <sys/mount.h>):

typedef enum : uint32_t { … } foo_enum;

Annoyingly the framework pulls in many of the structures the kernel exposes, but necessarily so - you need every stat, open, mount etc.

I’d guess that Clang supports this even in C, but it’s certainly non standard. Seems possible to support though, as long as the stdint types are understood.

I’m glad I’ve managed to spark a second successful nerdsniping this week after this! Usually @jackdk is the only I can reliably nerdsnipe.

Damn, all you people out here doing all this amazing Haskell (and C!) stuff while I’m stuck wrangling unpredictable Python builds and awful tools; making me very jealous and missing Haskell more than ever.

1 Like

Hah :smile: Well glad to help. I wasn’t aware of that alternative enum syntax, but I just tried it, and it Just Works ™: running

#include <stdint.h>

typedef enum : uint32_t { A, B, C } foo_enum;

through hs-bindgen results in (tidied up a bit for readability):

newtype Foo_enum = Foo_enum Word32
  deriving stock (Eq, Ord)

instance Storable Foo_enum where (..)
instance HsBindgen.Runtime.CEnum Foo_enum where (..)
instance HsBindgen.Runtime.SequentialCEnum Foo_enum where (..)

instance Show Foo_enum where showsPrec = HsBindgen.Runtime.showsCEnum
instance Read Foo_enum where readPrec  = HsBindgen.Runtime.readPrecCEnum

pattern A, B, C :: Foo_enum
pattern A = Foo_enum 0
pattern B = Foo_enum 1
pattern C = Foo_enum 2

(The CEnum and SequentialCEnum classes are from the the hs-bindgen-runtime package; showCEnum and readCEnum will use the names A .. C where possible instad of numbers. We don’t generate an ADT for enums in hs-bindgen, because C enums don’t restrict the range of the type, just give names to some values.)

Thanks for the example, I’ve added it to our test suite.

2 Likes

(I realise we’ve somewhat hijacked @romes’ original post, sorry about that, please let me know if we should move this to a thread of its own/github/somewhere)

@edsko That’s great news! I assume this is because hs-bindgen is built on top of libclang and lets it do all the hard work of understanding C?

You caught me in the middle of giving hs-bindgen a go, so I can probably give you many (many) more test cases if you’d like! :smile:

I’m struggling a bit with limiting which types hs-bindgen-cli looks at, if there a way to say "give me definitions for everything from this (potentially #included) file, but don’t try to do everything that those types include? For example

typedef struct {
	struct statfs *_Nonnull statfs;
	es_mount_disposition_t disposition; /* field available only if message version >= 8 */
	uint8_t reserved[60];
} es_event_mount_t;

Can I say I’d like bindings for es_event_mount_t but not struct statfs, just give me opaque pointers or something? I’ve been playing around the with the various --select options, but can’t seem to find the middle ground of get everything defined anywhere in macOS’s SDK, or get nothing at all.

My current testing is using

export MOD=EndpointSecurity; 
cabal run hs-bindgen-cli -- preprocess \
    --target aarch64-apple-macosx \
    --module $MOD -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include \
    -I /nix/store/c5iby6817ak2wr3x3b3qvybsknp8xkjm-clang-19.1.5-lib/lib/clang/19/include/ \
    --select-by-element-name '(enum |struct )?(es|ES).*' \
    --enable-blocks \
    --clang-option -mmacos-version-min=13.0 ./$MOD.h -o $MOD.hs \
    --gen-binding-spec $MOD.hs.spec

With EndpointSecurity.h just looking like.

#include <EndpointSecurity/EndpointSecurity.h>

Transitively that imports nearly everything provided by the OS/kernel.

Running it, I get a lot of warnings, and maybe 100 or so errors like:

[Error  ] [HsBindgen] Missing declaration: 'struct es_fd_t'; did you select the declaration?
[Error  ] [HsBindgen] Missing declaration: 'struct es_event_exec_t'; did you select the declaration?
[Error  ] [HsBindgen] Missing declaration: 'struct es_string_token_t'; did you select the declaration?
[Error  ] [HsBindgen] Missing declaration: 'struct es_event_exec_t'; did you select the declaration?

The other thing that seems to be causing plenty of issues is declarations like

OS_EXPORT
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios)
API_UNAVAILABLE(tvos, watchos)
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events, uint32_t event_count);

// Expands to
extern __attribute__((__visibility__("default")))
__attribute__((availability(macos,introduced=10.15)))
__attribute__((availability(ios,unavailable)))
__attribute__((availability(tvos,unavailable))) __attribute__((availability(watchos,unavailable)))
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events, uint32_t event_count);

which produce errors like

[Warning] [HsBindgen] Reparse error: "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/EndpointSecurity/ESClient.h" (line 35, column 20):
  unexpected out of scope type specifier macro name "macos"
  expecting function parameter
  OS_EXPORT API_AVAILABLE ( macos ( 10.15 ) ) API_UNAVAILABLE ( ios ) API_UNAVAILABLE ( tvos , watchos ) es_return_t es_subscribe ( es_client_t * _Nonnull client , const es_event_type_t * _Nonnull events , uint32_t event_count )

This definitely feels like I’ve probably bitten off more than I can chew, but if there is a way to limit what gets examined, I might be able to get somewhere. I did see there’s a spec file, but haven’t looked into how I’d go about using that (I managed to make one when using a much simpler include header, so I guess Apple haven’t broken all standards).

1 Like

It was @romes who pointed me to this thread in the first place and asked me to take a look at blocks and respond here, so I think he’ll be okay with it :slight_smile: But perhaps we should nonetheless take this elsewhere, perhaps the hs-bindgen issue tracker; I’m also happy to have a chat over Google Meet to discuss, if that would be helpful.

That said, let me address your current questions here.

  1. Selection. As you have already discovered, we offer various selection mechanisms, either file based (“everything from this file, but not from that..”), or name based (“all types starting with ES”). The other option that might be relevant here is --enable-program-slicing, which says “in addition to the things I have explicitly selected, also select anything that those definitions depend on”.

  2. Leaving types opaque. One thing we do not yet offer is an easy way to leave a type as opaque; We have a ticket open for this (#809), but I had currently marked it as low priority, because I didn’t really see much use for it. However, your use case is making me realize a benefit of this feature that I had not previously realized (even though it’s obvious in hindsight): it means any of its dependencies can then be skipped! I will add this to the issue and make it as high priority instead.

  3. Binding specifications. An important feature of hs-bindgen is “binding specifications”; think of these as something like “module signatures”; they describe what bindings exist for a particular header or set of headers, so that we can then make use of these existing bindings when generating new bindings: they make binding generation compositional. You could use this to say “this particular type (which I want to leave opaque) I want you to map to type such-and-such instead of generating a new type for it”. This works today, but unfortunately there is currently a bug with program slicing (#910) which means that if you do use such an external binding, the existing type will indeed be used, but any of its dependencies will still be pulled in.

  4. Macro warnings. Some C libraries define cpp macros that are part of the public API, and should ideally have “bindings” generated for them. However, macros have no types associated with them, and so what kind of Haskell binding should we generate for them? To solve this problem, hs-bindgen actually applies type inference, and if we can infer a type, then we generate Haskell type that mirrors the semantics of the macro. Of course, we can only do this for a (well-defined but small) subset of all macros, so seeing lots of warnings here is expected. We actually have a ticket open (#884) to make these warnings visible only when verbosity is increased.

So you are using hs-bindgen at a point in its lifecycle where it’s starting to become usable, and we have reached near feature completeness for all of C, but there are still usability issues to be sorted out (indeed, this is the first time I’m discussing it “in public” :grimacing:). That said, I think your use case ought to be possible with hs-bindgen is it is now. You won’t be able to use program slicing until #809/#910 is fixed, but if you use a combination of

  • a careful selection predicate, selecting only the definitions that you want
  • combined with an external binding specification for the types you want to leave opaque, mapping you a type that you define

I think it should work. To see what these binding specifications look like, either look at the fixtures (the .yaml files) for our golden tests, or else run hs-bindgen on a simple example header with the --gen-binding-spec option.

HTH,

-Edsko

2 Likes