• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

nanotech/swift-haskell-tutorial: Integrating Haskell with Swift Mac Apps

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

nanotech/swift-haskell-tutorial

开源软件地址(OpenSource Url):

https://github.com/nanotech/swift-haskell-tutorial

开源编程语言(OpenSource Language):

Haskell 36.9%

开源软件介绍(OpenSource Introduction):

Integrating Haskell with Swift Mac Apps

In this tutorial, we'll create a Mac app using Swift and Xcode to build the UI, and Haskell to implement backend logic.

A basic familiarity with Haskell, Swift, Objective-C, C, Xcode, and shell scripting will be assumed.

Table of Contents

  1. Project Setup
  2. Exporting Haskell Functions
  3. Importing Haskell's Generated FFI Headers into Swift
  4. Converting the Swift App to a Framework
    1. Framework Configuration
    2. App Bundle Configuration
  5. Linking to the Framework
  6. Starting Cocoa
  7. Linking to the Executable
  8. Calling Haskell from Swift
  9. Passing Complex Data Types
    1. Bytes
    2. Functions and Closures
  10. Troubleshooting

Project Setup

To start, create a new Cocoa Application Xcode project

Select the Cocoa app template

with Swift as the default language.

Select Swift as the default language

Name the project SwiftHaskell.

cd into the directory with the .xcodeproj and create a new stack project:

$ cd SwiftHaskell
$ stack new SwiftHaskellLibrary simple

Move these files up to the top directory, so we can run all of our commands from the same directory:

$ mv -vn SwiftHaskellLibrary/* .
$ rmdir SwiftHaskellLibrary

In SwiftHaskellLibrary.cabal, rename the executable to match the Xcode app's name of SwiftHaskell:

executable SwiftHaskell

To combine our Haskell library with our Swift UI, we'll build the Swift app as a framework and link to it from the Haskell executable. Xcode will then package both up into an app bundle.

The reason for doing the linking in this direction is that building a self-contained dynamic library is currently simpler with Swift and Xcode than it is with Cabal.

Exporting Haskell Functions

Here's the trivial function square that we'll export as a simple first example:

square x = x * x

Haskell functions exported via the FFI can only contain certain types in their signatures that are compatible with C: primitive integers, floats and doubles, and pointer types. The full list is in section 8.7 of the Haskell Report.

Since we'll only be using square to demonstrate the FFI, let's assign it a FFI-compatible type directly. For more complex functions, wrap them in a new function and convert their inputs and outputs as needed.

import Foreign.C

square :: CInt -> CInt
square x = x * x

To export square, add a foreign export definition with a calling convention of ccall:

foreign export ccall square :: CInt -> CInt

For the full syntax of foreign export, see section 8.3 of the Haskell Report.

Together, src/Main.hs is

module Main where

import Foreign.C

foreign export ccall square :: CInt -> CInt

square :: CInt -> CInt
square x = x * x

main :: IO ()
main = do
  putStrLn "hello world"

Importing Haskell's Generated FFI Headers into Swift

If we now stack build, in addition to building the library, GHC will generate C header files for each module with foreign exports. Because these are build artifacts, they're buried somewhat deep in the file hierarchy, but we can ask stack and find where they are:

$ find "$(stack path --dist-dir)" -name Main_stub.h
.stack-work/dist/x86_64-osx/Cabal-1.24.0.0/build/SwiftHaskellLibrary/SwiftHaskellLibrary-tmp/Main_stub.h

These stub headers #include "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

Since we'll be importing these headers into a Swift framework, we won't be able to use #include as we would in C. Instead, Swift uses Clang's module format. (Swift applications can use bridging headers, but frameworks must use modules.) A module.modulemap file to import Main_stub.h looks like

module SwiftHaskell {
    header "Main_stub.h"
    export *
}

As the paths to these headers vary, let's use a script to automatically copy them out and build a module map. We'll also create a symlink to the built executable's location for later.

#!/usr/bin/env bash
set -eu

# Change EXECUTABLE_NAME to the name of your Haskell executable.
EXECUTABLE_NAME=SwiftHaskell

DIST_DIR="$(stack path --dist-dir)"
GHC_VERSION="$(stack exec -- ghc --numeric-version)"
GHC_LIB_DIR="$(stack path --compiler-bin)/../lib/ghc-$GHC_VERSION"
STUB_BUILD_DIR="${DIST_DIR}/build/${EXECUTABLE_NAME}/${EXECUTABLE_NAME}-tmp"
STUB_MODULE_DIR="${EXECUTABLE_NAME}/include"
STUB_MODULE_MAP="${STUB_MODULE_DIR}/module.modulemap"

# Create a module map from the generated Haskell
# FFI export headers for importing into Swift.
mkdir -p "${STUB_MODULE_DIR}"
NL="
"
module_map="module ${EXECUTABLE_NAME} {${NL}"
for h in $(find "${STUB_BUILD_DIR}" -name '*.h'); do
    h_filename="${h/$STUB_BUILD_DIR\//}"
    cp "$h" "${STUB_MODULE_DIR}/"
    module_map="${module_map}    header \"${h_filename}\"${NL}"
done
module_map="${module_map}    export *${NL}"
module_map="${module_map}}"
echo "${module_map}" > "${STUB_MODULE_MAP}"

# Symlink to the current GHC's header directory so we can add it
# to Xcode's include path as $(PROJECT_DIR)/build/ghc/include.
mkdir -p build/ghc
ln -sf "${GHC_LIB_DIR}/include" build/ghc/

# Symlink to the Haskell executable so we can easily drag it
# into Xcode to use as the executable for the app bundle.
ln -sf "../${DIST_DIR}/build/${EXECUTABLE_NAME}/${EXECUTABLE_NAME}" build/

Change the value of the EXECUTABLE_NAME variable to the name of the executable in your .cabal file if you named it something other than SwiftHaskell.

Save the script as link-deps.sh, run stack build, and then run bash link-deps.sh to prepare for the next section.

Running the script will create SwiftHaskell/include/module.modulemap and two symlinks in the project's build/ directory:

  • SwiftHaskell -> ../.stack-work/dist/{arch}/Cabal-{version}/build/SwiftHaskell/SwiftHaskell
  • ghc
    • include -> ~/.stack/programs/{arch}/ghc-{version}/bin/../lib/ghc-{version}/include

Text marked as {} will vary.

Converting the Swift App to a Framework

Create a new Cocoa Framework target in the Xcode project,

Select the Cocoa Framework template

with Swift as the default language.

Select Swift as the default language

Name the framework SwiftAppLibrary.

Framework Configuration

To move over our application code and UI, change the Target Membership of AppDelegate.swift and MainMenu.xib in Xcode's File Inspector in the right sidebar so that they are only included in SwiftAppLibrary:

Target Membership

In AppDelegate.swift, remove the @NSApplicationMain attribute from the AppDelegate class, as we don't want an auto-generated main function in our framework. We will implement an equivalent way to start Cocoa later, to be called from the Haskell executable's main.

Xcode will place the built framework in a temporary directory (~/Library/Developer/Xcode/DerivedData/) with an unpredictable subpath. So that Cabal will be able to find the framework for linking, add a new Run Script build phase

New Run Script Phase

that creates a symlink to the built framework in build/:

set -u
ln -sf "${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}" "${PROJECT_DIR}/build/"

App Bundle Configuration

Drag the SwiftHaskell executable we built previously with Stack into Xcode from the build/ directory that we symlinked it into,

The SwiftHaskell executable in Xcode

but do not add it to any targets when prompted:

Do not add the executable to any targets

When Xcode creates Swift frameworks, it expects that the application that links the framework will include the Swift standard libraries. Xcode automatically adds these libraries to Swift applications. Since our application executable is built with Haskell and not Swift, we'll need to explicitly tell Xcode to include the Swift standard libraries in our application.

In the app target's Build Settings tab, set Always Embed Swift Standard Libraries to Yes:

Always Embed Swift Standard Libraries: Yes

In the app target's Build Phases, remove the Compile Sources and Link Binary With Libraries phases. We are using Stack to build the app's executable instead of Xcode.

Add a new Copy Files phase that copies the SwiftHaskell executable into the app bundle's Executables directory:

New Copy Files Phase

Select the executable

Copy into Executables

Build the SwiftAppLibrary framework in Xcode to prepare for the next sections.

Select the SwiftAppLibrary target

Linking to the Framework

Add these options to the executable's section in the .cabal file:

executable SwiftHaskell
  ghc-options:         -threaded -framework-path build
  ld-options:          -rpath @executable_path/../Frameworks
  frameworks:          SwiftAppLibrary
  • -threaded enables the multithreaded GHC runtime, which is usually what you want.
  • -framework-path build tells GHC to look for frameworks where we symlinked our framework to.
  • -rpath @executable_path/../Frameworks embeds into the executable where the dynamic linker should look for shared libraries.

See the Flag Reference in the GHC Users' Guide and man 1 ld for more details.

Starting Cocoa

Because Haskell has control over the program's entry point (main), we'll need to have it call out to Cocoa to start its main thread. In SwiftAppLibrary.h, declare a new function named runNSApplication and mark it as FOUNDATION_EXPORT to indicate that it should be exported from the framework:

FOUNDATION_EXPORT void runNSApplication(void);

Implement the function by adding a new Objective-C .m file to the framework target containing

#import "SwiftAppLibrary.h"

@interface AClassInThisFramework : NSObject @end
@implementation AClassInThisFramework @end

void runNSApplication(void) {
    NSApplication *app = [NSApplication sharedApplication];
    NSBundle *bundle = [NSBundle bundleForClass:[AClassInThisFramework class]];
    NSArray *topObjects;
    [[[NSNib alloc] initWithNibNamed:@"MainMenu" bundle:bundle]
     instantiateWithOwner:app topLevelObjects:&topObjects];
    [app run];
}

This is possible to write in Swift, however as of Swift 3.0.2, the annotation to export unmangled C symbols (@_cdecl) is not documented as stable. Additionally, whole module optimization will assume that @_cdecl symbols are unused and remove them.

In Main.hs, import the foreign function and call it from the end of main:

module Main where

import Foreign.C

foreign export ccall square :: CInt -> CInt

square :: CInt -> CInt
square x = x * x

foreign import ccall "runNSApplication" runNSApplication :: IO ()

main :: IO ()
main = do
  putStrLn "hello world"
  runNSApplication

runNSApplication will not return, being busy with Cocoa's main run loop. Use Control.Concurrent.forkIO before calling runNSApplication to run other tasks as needed.

Build the SwiftHaskellLibrary framework in Xcode, then stack build, and then finally build and run the SwiftHaskell app target to launch the app and see the default window from MainMenu.xib:

A blank window

Linking to the Executable

To tell Xcode where to find our module.modulemap, add $(PROJECT_DIR)/SwiftHaskell/include to the framework target's Swift Compiler - Search Paths, Import Paths setting in Xcode,

The Swift module import paths

and, for the GHC headers the module depends on, add $(PROJECT_DIR)/build/ghc/include to the framework's User Header Search Paths setting:

The User Header Search Paths

In order for the framework to be able to link to symbols in the Haskell executable, we need to tell the linker to leave symbols undefined and have them be resolved at runtime.

Add -undefined dynamic_lookup to the framework's Other Linker Flags setting.

Be aware that this means that link errors will occur at runtime instead of at link time. Also note that the framework linking to symbols in the executable (and depending on the generated headers), and the executable linking to the framework, creates a circular dependency. When building the project clean, you will need to build the components in this order:

  • stack build to generate the Haskell FFI export headers. Linking will fail, as the Swift framework is not built yet.
  • Build the Swift framework.
  • stack build
  • Build the app bundle.

The first step can be skipped subsequently by committing the generated headers to source control.

To help automate this, add a new Run Script build phase to the beginning of the framework's build phases with the contents

stack build
bash link-deps.sh

Add the Haskell sources as input files:

$(PROJECT_DIR)/src/Main.hs
$(PROJECT_DIR)/SwiftHaskellLibrary.cabal
$(PROJECT_DIR)/stack.yaml

And the executable as an output file:

$(PROJECT_DIR)/build/SwiftHaskell

Or, if you prefer building primarily with stack build, set the build-type in your .cabal to Custom and add a postBuild hook to Setup.hs:

import Distribution.Simple
import System.Process

main = defaultMainWithHooks $ simpleUserHooks
  { postBuild = \args buildFlags pkgDesc localBuildInfo -> do
      callProcess "bash" ["link-deps.sh"]
      callProcess "xcodebuild" ["-target", "SwiftHaskell"]
  }

Calling Haskell from Swift

We're now ready to use exported Haskell functions from Swift. Import the module we defined in our module.modulemap, SwiftHaskell, at the top of AppDelegate.swift:

import SwiftHaskell

Let's add a new label to the window for us to write the result of our Haskell function square into. Open MainMenu.xib and select the window object in the left sidebar to bring it into view. Then drag in a label from the object library in the right sidebar into the window:

Add a label

Now option-click on AppDelegate.swift in the file list to open it in an assistant editor. Holding the control key, drag the label from the window into the AppDelegate class to add and connect a new @IBOutlet:

Add and connect the label outlet

Name the outlet

Adding the outlet will add a new property to the AppDelegate:

@IBOutlet weak var label: NSTextField!

With the SwiftHaskell module imported and a label connected, let's call square and display its result in the label. Add this to applicationDidFinishLaunching:

label.stringValue = "\(square(5))"

The final contents of AppDelegate.swift are:

import Cocoa
import SwiftHaskell

class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
    @IBOutlet weak var label: NSTextField!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        label.stringValue = "\(square(5))"
    }

    func applicationWillTerminate(_ aNotification: Notification) {
    }
}

Running the app,

5 squared

If the build fails with Use of unresolved identifier 'square', perform a full clean with the Product » Clean Build Folder... ⌥⇧⌘K menu command and then rebuild. (Hold ⌥ option to reveal the menu item.) This appears to be a bug with Xcode (version 8.2 as of writing) caching some intermediate state from before the SwiftHaskell module was fully configured, and should not occur in future builds.

Passing Complex Data Types

Bytes

[UInt8] to ByteString

Call withUnsafeBufferPointer on a Swift Array to get an UnsafeBufferPointer, and then read its .baseAddress property to get an UnsafePointer pass into the exported Haskell function. The corresponding mutable variants are withUnsafeMutableBufferPointer, UnsafeMutableBufferPointer, and UnsafeMutablePointer.

The generated Haskell headers use a single pointer type for all pointers, HsPtr (void *), which is mutable (not const). If you know that a function does not mutate through a pointer, you can use the HsPtr(mutating:) constructor to cast a non-mutable pointer to a mutable pointer.

bytes.withUnsafeBufferPointer { bytesBufPtr in
    someHaskellFunction(HsPtr(mutating: bytesBufPtr.baseAddress), bytesBufPtr.count)
}

If the function mutates the pointer's data, you must use withUnsafeMutableBytes:

bytes.withUnsafeMutableBufferPointer { bytesBufPtr in
    someHaskellFunction(bytesBufPtr.baseAddress, bytesBufPtr.count)
}

To bring an array of bytes into a Haskell ByteString, use Data.ByteString.packCStringLen:

type CString = Ptr CChar
packCStringLen :: (CString, Int) -> IO ByteString

For example,

import Foreign.C
import Foreign.Ptr

import qualified Data.ByteString as B
import Data.Word

foreign export ccall countBytes :: Word8 -> Ptr CChar -> CSize -> IO CSize

countBytes :: Word8 -> Ptr CChar -> CSize -> IO CSize
countBytes needle haystack haystackLen = do
  s <- B.packCStringLen (haystack, fromIntegral haystackLen)
  pure (B.foldl (\count b -> count + if b == needle then 1 else 0) 0 s)

With a Swift wrapping function of

func count(byte: UInt8, in bytes: [UInt8]) -> Int {
    var r = 0
    bytes.withUnsafeBytes { bytesPtr in
        r = Int(SwiftHaskell.countBytes(byte, HsPtr(mutating: bytesPtr.baseAddress)))
    }
    return r
}

ByteString to [UInt8]

To pass a ByteString to an exported Swift function that accepts a pointer and a length, use useAsCStringLen:

import Data.ByteString (ByteString)
import qualified Data.ByteString as B

foreign import ccall "someSwiftFunction" someSwiftFunction :: Ptr CChar -> CSize -> IO ()

passByteString :: ByteString -> IO ()
passByteString s =
  B.useAsCStringLen s $ \(p, n) ->
    someSwiftFunction p (fromIntegral n)

To return the contents of a ByteString, call mallocArray to allocate a new array with C's malloc allocator and copy the ByteString data into it. The Swift caller is then responsible for calling free on the pointer. Use Foreign.Storable.poke to also return the size by writing into a passed pointer.

import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as BU
import Foreign.Storable (poke)

mallocCopyByteString :: ByteString -> IO (Ptr CChar, Int)
mallocCopyByteString s =
  BU.unsafeUseAsCStringLen s $ \(p, n) -> do
    a <- mallocArray n
    copyArray a p n
    pure (a, n)

foreign export ccall getSequence :: Ptr CSize -> IO (Ptr CChar)

getSequence :: Ptr CSize -> IO (Ptr CChar)
getSequence sizePtr = do
  (p, n) <- mallocCopyByteString (B.pack [1..10])
  poke sizePtr (fromIntegral n)
  pure p

The imported getSequence function returns a UnsafeMutableRawPointer in Swift. To copy the elements into a Swift array, first assign a type to the memory using the .assumingMemoryBound(to:) method. Then wrap the pointer and length in an UnsafeBufferPointer and pass it to the array constructor, which copies the elements into a ne


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap