Using bindgen with System Frameworks on macOS

Created On:

Recently I was using IOKit in Rust and I found that existing FFI bindings on crates.io were not sufficient for my use case. I had two options:

Since IOKit has a large and complex API surface, I felt that creating them by hand would be too cumbersome, so I ended up looking at bindgen.

The premise of bindgen is pretty simple, given a C header file, it will parse the header file and output Rust functions and structures that allow for FFI. Underneath the hood, bingen relies on libclang to parse the headers and then given the parsed results, it produces Rust code for FFI.

However, on macOS bindgen fails out of the box to generate bindings for headers that are located in the macOS SDK and this is because Apple’s provided libclang does not have the same logic as Apple’s provided clang on where to find Framework headers.

A simple example can show this failure. A wrapper.h file which references a macOS SDK provided header like CoreFoundation.h will fail with bindgen even though clang can process it successfully.

$ echo '#include <CoreFoundation/CoreFoundation.h>' > wrapper.h
$ # Ask Apple's clang to expand the header
$ clang -E  wrapper.h > /dev/null
$ echo $?
0

Successfully processing the wrapper.h file with clang

However passing the same wrapper.h to bindgen results in an error.

$ bindgen wrapper.h
wrapper.h:1:10: fatal error: 'CoreFoundation/CoreFoundation.h' file not found
wrapper.h:1:10: note: did not find header 'CoreFoundation.h' in framework 'CoreFoundation' (loaded from '/System/Library/Frameworks')
wrapper.h:1:10: fatal error: 'CoreFoundation/CoreFoundation.h' file not found, err: true
thread 'main' panicked at 'Unable to generate bindings: ()', /Users/zmanji/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.58.1/src/main.rs:54:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Failing to process the wrapper.h with bindgen

The libclang used by bindgen is searching in /System/Library/Frameworks which does not contain headers. Ever since macOS 10.14 Apple stopped placing headers in /usr/include and /System/Library/Frameworks. Instead the headers are located in the SDK directories. Apple’s clang knows to check in these directories.

$ clang -H  wrapper.h
. /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CoreFoundation.h
...

The fix is to direct bindgen and libclang to the SDK directory which has the headers. It might be temping to hard-code the Xcode.app directory however headers can also be installed by the Command Line Tools for Xcode which would be located in the /Library/Developer/CommandLineTools directory.

The fix would be not to hard code these directories but instead query the SDK path with the xcrun tool. From the man page

xcrun provides a means to locate or invoke developer tools from the command-line, without requiring users to modify Makefiles or otherwise take inconvenient measures to support multiple Xcode tool chains.

Using xcrun the SDK path can be obtained not only for the macOS SDK but for the iOS SDK and other Apple platforms.

$ xcrun --sdk macosx --show-sdk-path
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk
$ xcrun --sdk iphoneos --show-sdk-path
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.5.sdk

The path can be passed to bindgen to ensure it can find the headers.

$ bindgen input.h -- -isysroot$(xcrun --sdk macosx --show-sdk-path)
/* automatically generated by rust-bindgen 0.58.1 */
...

To conclude, bindgen on macOS does not work out of the box for System Frameworks. The only way for bindgen to process the framework headers is to be told where the macOS SDK path is and a way to do that is to use xcrun. Using xcrun will work regardless if the OS has a full Xcode.app installation or just the Command Line Tools for XCode installed.