Files

205 lines
6.8 KiB
Markdown

> Write or rewrite a tutorial that covers building a project
> that interfaces haskell backend code with Swift GUI code in an
> XCode 8.2 project. Bonus points if the entire thing can be
> built from the command line `stack build` rather than inside
> XCode.
# Integrating Haskell with Swift Mac Apps
To start, let's create a new Xcode project:
![Select the Cocoa app template](tutorial/xcode-cocoa-template.png)
![Create a new project](tutorial/xcode-create-project.png)
Then `cd` into the directory with the `.xcodeproj` and create a
new stack project:
$ cd SwiftHaskell
$ stack new SwiftHaskellLibrary simple-library
Let's move these files up to the top directory, so we can
run both `stack` and `xcodebuild` from the same directory:
$ mv -vn SwiftHaskellLibrary/* .
$ rmdir SwiftHaskellLibrary
## Exporting Haskell Functions to Swift
Add a `foreign export` function to the Haskell library.
TODO: Explain exporting using Haskell's C FFI
module Lib where
import Foreign.C
foreign export ccall square :: CInt -> CInt
square :: CInt -> CInt
square x = x * x
If we `stack build` now, in addition to building the library,
GHC will generate a C header file for us to include. Because
it's a build artifact, it's buried somewhat deep in the file
hierarchy, but we can ask `stack` where it is:
$ find "$(stack path --dist-dir)" -name Lib_stub.h
.stack-work/dist/x86_64-osx/Cabal-1.24.0.0/build/Lib_stub.h
The stub header includes `HsFFI.h` from GHC, so we'll also need
to find the current compiler's version of that header.
$ find "$(stack path --compiler-bin)/.." -name HsFFI.h
/Users/nanotech/.stack/programs/x86_64-osx/ghc-8.0.1/bin/../lib/ghc-8.0.1/include/HsFFI.h
Let's make a script to symlink to these from a more convenient
location so we don't need to change the Xcode project when the
compiler or Cabal version changes:
#!/bin/sh
set -eux
DIST_DIR="$(stack path --dist-dir)"
ln -sf ../"$DIST_DIR"/build/Lib_stub.h build/
ln -sf "$GHC_LIB_DIR"/include build/
Run it, then add `$(PROJECT_DIR)/build` and
`$(PROJECT_DIR)/build/include` to the target's *User Header
Search Paths* in Xcode:
![The User Header Search Paths](tutorial/xcode-header-search-paths.png)
Now that we have the exported definitions in a header file, we
need to import them into Swift. Create a new header file in
Xcode named `SwiftHaskell-Bridging-Header.h` and save it in the
same directory as `AppDelegate.swift`. Then set it as the
*Objective-C Bridging Header* in Xcode:
![Set the target's Objective-C Bridging Header](tutorial/xcode-bridging-header.png)
And include the Haskell stub header:
#ifndef SwiftHaskell_Bridging_Header_h
#define SwiftHaskell_Bridging_Header_h
#include "Lib_stub.h"
#endif /* SwiftHaskell_Bridging_Header_h */
## Linking
To link our Haskell library into our Swift app, we'll build it
as a dynamic library and add it to our Xcode project. Building a
static library is also possible, but currently requires
[rebuilding the GHC standard libraries][pic-ghc].
[pic-ghc]: https://github.com/lyokha/nginx-haskell-module#static-linkage-against-basic-haskell-libraries
Add a `ghc-options` line to the `library` section in the
`.cabal` file:
library
# ... other options
ghc-options: -threaded -dynamic -shared -fPIC -o build/libswifthaskell.dylib
- `-threaded` enables the multithreaded GHC runtime, which is
usually what you want.
- `-dynamic` tells GHC to link to the dynamic versions of
Haskell libraries. This is required when using `-shared`.
- `-shared` builds a shared library.
- `-fPIC` enables position-independent code, which is needed for
shared libraries.
Run `stack build`, then drag `build/libswifthaskell.dylib` into
the Xcode project and add it to the SwiftHaskell target.
We'll also need to link to the RTS so we can initialize it
from Swift, and copy all shared library dependencies into the
app bundle so it's self-contained. `otool -L` will show what
libraries `libswifthaskell.dylib` depends on:
$ otool -L build/libswifthaskell.dylib
build/libswifthaskell.dylib:
@rpath/libswifthaskell.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libHSbase-4.9.0.0-ghc8.0.1.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libHSinteger-gmp-1.0.0.1-ghc8.0.1.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libHSghc-prim-0.5.0.0-ghc8.0.1.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)
Let's update our previous script to locate them and symlink them
into the build directory:
#!/bin/sh
set -eux
DIST_DIR="$(stack path --dist-dir)"
GHC_VERSION="$(stack exec -- ghc --numeric-version)"
GHC_LIB_DIR="$(stack path --compiler-bin)/../lib/ghc-$GHC_VERSION"
ln -sf ../"$DIST_DIR"/build/Lib_stub.h build/
ln -sf "$GHC_LIB_DIR"/include build/
ln -sf "$GHC_LIB_DIR"/rts/libHSrts_thr-ghc"$GHC_VERSION".dylib build/
# FIXME: Link the other Haskell libraries
ln -sf "$GHC_LIB_DIR"/rts/libffi.dylib build/
This is the `symlink_deps.sh` script in the repository.
TODO: Find shared library dependencies automatically, like
`cabal-macosx`.
Drag these libraries into Xcode too,
![Xcode files](tutorial/xcode-files.png)
Then go to the target's Build Phases panel and add a new phase
to copy the libraries into the app bundle:
![Add a new Copy Files phase](tutorial/xcode-new-copy-files-phase.png)
![Add the libraries to the phase](tutorial/xcode-copy-frameworks.png)
## Runtime Setup
Before using functions from Haskell, we'll need to start its
runtime. `hs_init` takes pointers to C's argc and argv. In
Swift, these are available as `CommandLine.argc` and
`CommandLine.unsafeArgv`.
Add this to your app delegate's `applicationDidFinishLaunching`
method:
func applicationDidFinishLaunching(_ aNotification: Notification) {
var argc = CommandLine.argc
var argv = Optional.some(CommandLine.unsafeArgv)
hs_init(&argc, &argv)
}
And the corresponding `hs_exit` to `applicationWillTerminate`:
func applicationWillTerminate(_ aNotification: Notification) {
hs_exit()
}
## Calling Haskell from Swift
Add a new label to the window in `MainMenu.xib` for us to write
the result of our Haskell function `square` into, and add it as
an `@IBOutlet` to the `AppDelegate`:
@IBOutlet weak var label: NSTextField!
We already have our Haskell library's header imported, so we
can just call the exported `square` function. Add this to
`applicationDidFinishLaunching`:
label.stringValue = "\(square(5))"
And run the app:
![5 squared](tutorial/squared.png)
## Calling Swift from Haskell
TODO: FunPtr callbacks