diff --git a/README.md b/README.md index e69de29..205ba0c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,413 @@ +# Integrating Haskell with Swift Mac Apps + +To start, create a new Cocoa Application Xcode project +with Swift as the default language: + +![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: + +```sh +$ 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: + +```sh +$ mv -vn SwiftHaskellLibrary/* . +$ rmdir SwiftHaskellLibrary +``` + +In `SwiftHaskellLibrary.cabal`, rename the executable to +match the Xcode app's name of SwiftHaskell: + +```cabal +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: + +```haskell +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][haskell-report-8.7]. + +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. + +```haskell +import Foreign.C + +square :: CInt -> CInt +square x = x * x +``` + +To export `square`, add a `foreign export` definition with a +calling convention of `ccall`: + +```haskell +foreign export ccall square :: CInt -> CInt +``` + +For the full syntax of `foreign export`, see [section 8.3 of the +Haskell Report][haskell-report-8.3]. + +[haskell-report-8.7]: https://www.haskell.org/onlinereport/haskell2010/haskellch8.html#x15-1700008.7 +[haskell-report-8.3]: https://www.haskell.org/onlinereport/haskell2010/haskellch8.html#x15-1530008.3 + +Together, `src/Main.hs` is + +```haskell +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][clang-modules]. (Swift +applications can use [bridging headers][swift-bridging-headers], +but frameworks [must use modules][so-non-modular-header].) A +`module.modulemap` file to import `Main_stub.h` looks like + + module SwiftHaskell { + header "Main_stub.h" + export * + } + +[swift-bridging-headers]: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html#//apple_ref/doc/uid/TP40014216-CH10-ID156 +[so-non-modular-header]: https://stackoverflow.com/questions/24103169/swift-compiler-error-non-modular-header-inside-framework-module/37072619#37072619 +[clang-modules]: http://clang.llvm.org/docs/Modules.html + +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. + +```bash +#!/usr/bin/env bash +set -eu + +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 from a more +# convenient place for Xcode to find. +mkdir -p build/ghc +ln -sf "${GHC_LIB_DIR}/include" build/ghc/ + +# Symlink to the Haskell executable for Xcode. +ln -sf "../${DIST_DIR}/build/${EXECUTABLE_NAME}/${EXECUTABLE_NAME}" build/ +``` + +Save the script as `link-deps.sh`, run `stack build`, and then +run `bash link-deps.sh` to prepare for the next section. + +## Converting the Swift App to a Framework + +Create a new Cocoa Framework target in the Xcode project +named SwiftAppLibrary, then change the Target Membership of +`AppDelegate.swift` and `MainMenu.xib` to only SwiftAppLibrary +in Xcode's File Inspector in the right sidebar: + +![Target Membership](tutorial/xcode-target-membership.png) + +In the new framework's build settings, set **Always Embed Swift +Standard Libraries** to **Yes**. + +Drag the `SwiftHaskell` executable we built previously with +Stack into Xcode from the `build/` directory that we symlinked +it into, but do not add it to any targets when prompted: + +![The SwiftHaskell executable in Xcode](tutorial/xcode-files-swifthaskell-executable.png) + +In the SwiftHaskell app target's Build Phases, remove the +**Compile Sources** and **Link Binary With Libraries** +phases, and add a new **Copy Files** phase that copies the +`SwiftHaskell` executable into the app bundle's Executables +directory: + +![Copy into Executables](tutorial/xcode-copy-files-swifthaskell-executable.png) + +Finally, in the SwiftAppLibrary framework target's Build Phases, +add a new **Run Script** phase to create a symlink to the built +framework for us to link to from Cabal: + +```sh +set -u +ln -sf "${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}" "${PROJECT_DIR}/build/" +``` + +## Linking to the Framework + +Add these options to the executable's section in the `.cabal` +file: + +```cabal +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. + +## Starting the GUI + +Because Haskell has control over the program's entry point +(`main`), we'll need to have it call out to Swift to +start Cocoa's main thread. Add this top level function to +`AppDelegate.swift`: + +```swift +@_cdecl("runNSApplication") +func runNSApplication() { + let app = NSApplication.shared() + var topObjects: NSArray = [] + NSNib.init(nibNamed: "MainMenu", bundle: Bundle(for: AppDelegate.self))! + .instantiate(withOwner: app, topLevelObjects: &topObjects) + app.run() +} +``` + +`@_cdecl` sets the symbol name of the Swift function, overriding +normal symbol mangling. + +In `SwiftAppLibrary.h`, add a `FOUNDATION_EXPORT` C prototype +for the function, so that its symbol gets marked as public and +is exported from the framework: + +```c +FOUNDATION_EXPORT void runNSApplication(void); +``` + +In `Main.hs`, import the foreign function and call it from +the end of `main`: + +```haskell +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. + +Run `stack build`, and build and run the `SwiftHaskell` app +target in Xcode to launch the app and see the default window +from `MainMenu.xib`: + +![A blank window](tutorial/empty-app.png) + +## Completing Linking Setup + +Add `$(PROJECT_DIR)/SwiftHaskell/include` to the framework +target's **Swift Compiler - Search Paths, Import Paths** setting +in Xcode, + +![The Swift module import paths](tutorial/xcode-swift-module-search-paths.png) + +and `$(PROJECT_DIR)/build/ghc/include` to the framework's **User +Header Search Paths** setting: + +![The User Header Search Paths](tutorial/xcode-header-search-paths.png) + +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 initially building the project, 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 + +```sh +stack build +bash link-deps.sh +``` + +Add the Haskell sources as input files: + +``` +$(PROJECT_DIR)/src/Main.hs +$(PROJECT_DIR)/SwiftHaskellLibrary.cabal +``` + +And the executable as an output file: + +``` +$(PROJECT_DIR)/build/SwiftHaskell +``` + +TODO: `xcodebuild` from `Setup.hs` hooks? + +## Calling Haskell from Swift + +We're now ready to use exported Haskell functions from Swift. +Import `SwiftHaskell` at the top of `AppDelegate.swift` + +```swift +import SwiftHaskell +``` + +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`: + +```swift +@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`: + +```swift +label.stringValue = "\(square(5))" +``` + +The final contents of `AppDelegate.swift` are: + +```swift +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) { + } +} + +@_cdecl("swiftAppMain") +func swiftAppMain() { + let app = NSApplication.shared() + var topObjects: NSArray = [] + NSNib.init(nibNamed: "MainMenu", bundle: Bundle(for: AppDelegate.self))! + .instantiate(withOwner: app, topLevelObjects: &topObjects) + app.run() +} +``` + +Running the app, + +![5 squared](tutorial/squared.png) + +## Strings and ByteString FFI + +TODO + +## Function and Closure FFI + +TODO + diff --git a/SwHaLib/include/module.modulemap b/SwHaLib/include/module.modulemap new file mode 100644 index 0000000..39283df --- /dev/null +++ b/SwHaLib/include/module.modulemap @@ -0,0 +1,3 @@ +module SwHaLib { + export * +} diff --git a/SwiftAppLibrary/AppDelegate.swift b/SwiftAppLibrary/AppDelegate.swift index b6a65cf..525bee4 100644 --- a/SwiftAppLibrary/AppDelegate.swift +++ b/SwiftAppLibrary/AppDelegate.swift @@ -7,25 +7,23 @@ // import Cocoa +import SwiftHaskell class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var window: NSWindow! @IBOutlet weak var label: NSTextField! - static var square: (@convention(c) (CInt) -> CInt)! = nil - func applicationDidFinishLaunching(_ aNotification: Notification) { - label.stringValue = "\(AppDelegate.square(5))" + label.stringValue = "\(square(5))" } func applicationWillTerminate(_ aNotification: Notification) { } } -@_cdecl("swiftAppMain") -func swiftAppMain(square: @escaping @convention(c) (CInt) -> CInt) { - AppDelegate.square = square +@_cdecl("runNSApplication") +func runNSApplication() { let app = NSApplication.shared() var topObjects: NSArray = [] NSNib.init(nibNamed: "MainMenu", bundle: Bundle(for: AppDelegate.self))! diff --git a/SwiftAppLibrary/SwiftAppLibrary.h b/SwiftAppLibrary/SwiftAppLibrary.h index 1b07ccb..9bfb10d 100644 --- a/SwiftAppLibrary/SwiftAppLibrary.h +++ b/SwiftAppLibrary/SwiftAppLibrary.h @@ -14,8 +14,6 @@ FOUNDATION_EXPORT double SwiftAppLibraryVersionNumber; //! Project version string for SwiftAppLibrary. FOUNDATION_EXPORT const unsigned char SwiftAppLibraryVersionString[]; -FOUNDATION_EXPORT void swiftAppMain(void); +FOUNDATION_EXPORT void runNSApplication(void); // In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/SwiftHaskell.xcodeproj/project.pbxproj b/SwiftHaskell.xcodeproj/project.pbxproj index 5387877..4eade78 100644 --- a/SwiftHaskell.xcodeproj/project.pbxproj +++ b/SwiftHaskell.xcodeproj/project.pbxproj @@ -11,8 +11,8 @@ BFABC4CD1E4D627E006036C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFABC4CC1E4D627E006036C6 /* AppDelegate.swift */; }; BFABC4D21E4D664D006036C6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFABC4D01E4D664D006036C6 /* MainMenu.xib */; }; BFABC4E01E4D781D006036C6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFABC4DF1E4D781D006036C6 /* Assets.xcassets */; }; - BFABC4ED1E4D78E5006036C6 /* SwiftHaskell in CopyFiles */ = {isa = PBXBuildFile; fileRef = BFABC4EA1E4D78D9006036C6 /* SwiftHaskell */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - BFABC4F01E4D7951006036C6 /* SwiftAppLibrary.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = BFABC45B1E4C1DD1006036C6 /* SwiftAppLibrary.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BFABC4ED1E4D78E5006036C6 /* SwiftHaskell in Copy Files */ = {isa = PBXBuildFile; fileRef = BFABC4EA1E4D78D9006036C6 /* SwiftHaskell */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + BFABC4F01E4D7951006036C6 /* SwiftAppLibrary.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFABC45B1E4C1DD1006036C6 /* SwiftAppLibrary.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -26,24 +26,26 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - BFABC4EC1E4D78DF006036C6 /* CopyFiles */ = { + BFABC4EC1E4D78DF006036C6 /* Copy Files */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 6; files = ( - BFABC4ED1E4D78E5006036C6 /* SwiftHaskell in CopyFiles */, + BFABC4ED1E4D78E5006036C6 /* SwiftHaskell in Copy Files */, ); + name = "Copy Files"; runOnlyForDeploymentPostprocessing = 0; }; - BFABC4EF1E4D7948006036C6 /* CopyFiles */ = { + BFABC4EF1E4D7948006036C6 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - BFABC4F01E4D7951006036C6 /* SwiftAppLibrary.framework in CopyFiles */, + BFABC4F01E4D7951006036C6 /* SwiftAppLibrary.framework in Embed Frameworks */, ); + name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -128,11 +130,12 @@ isa = PBXNativeTarget; buildConfigurationList = BFABC4631E4C1DD1006036C6 /* Build configuration list for PBXNativeTarget "SwiftAppLibrary" */; buildPhases = ( + BF2EA5CA1E5030E100651018 /* stack build and link-deps */, BFABC4561E4C1DD1006036C6 /* Sources */, BFABC4571E4C1DD1006036C6 /* Frameworks */, BFABC4581E4C1DD1006036C6 /* Headers */, BFABC4591E4C1DD1006036C6 /* Resources */, - BFABC4CE1E4D6344006036C6 /* ShellScript */, + BFABC4CE1E4D6344006036C6 /* Link framework to build */, ); buildRules = ( ); @@ -147,10 +150,9 @@ isa = PBXNativeTarget; buildConfigurationList = BFABC4E51E4D781D006036C6 /* Build configuration list for PBXNativeTarget "SwiftHaskell" */; buildPhases = ( - BFABC4F11E4D81E8006036C6 /* ShellScript */, BFABC4D51E4D781D006036C6 /* Resources */, - BFABC4EF1E4D7948006036C6 /* CopyFiles */, - BFABC4EC1E4D78DF006036C6 /* CopyFiles */, + BFABC4EF1E4D7948006036C6 /* Embed Frameworks */, + BFABC4EC1E4D78DF006036C6 /* Copy Files */, ); buildRules = ( ); @@ -221,7 +223,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - BFABC4CE1E4D6344006036C6 /* ShellScript */ = { + BF2EA5CA1E5030E100651018 /* stack build and link-deps */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(PROJECT_DIR)/src/Lib.hs", + "$(PROJECT_DIR)/src/Main.hs", + "$(PROJECT_DIR)/SwiftHaskellLibrary.cabal", + ); + name = "stack build and link-deps"; + outputPaths = ( + "$(PROJECT_DIR)/build/SwiftHaskell", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "stack build\nbash link-deps.sh"; + showEnvVarsInLog = 0; + }; + BFABC4CE1E4D6344006036C6 /* Link framework to build */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -229,25 +250,12 @@ inputPaths = ( "$(BUILT_PRODUCTS_DIR)/$(FULL_PRODUCT_NAME)", ); + name = "Link framework to build"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -eu\nrsync -a \"${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}\" \"${PROJECT_DIR}\"/"; - showEnvVarsInLog = 0; - }; - BFABC4F11E4D81E8006036C6 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "stack build\n./symlink_deps.sh"; + shellScript = "set -u\nln -sf \"${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}\" \"${PROJECT_DIR}/build/\""; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -288,10 +296,10 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; @@ -340,10 +348,10 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; @@ -384,7 +392,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; @@ -395,11 +402,17 @@ INFOPLIST_FILE = SwiftAppLibrary/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = ( + "-undefined", + dynamic_lookup, + ); PRODUCT_BUNDLE_IDENTIFIER = net.nanotechcorp.SwiftAppLibrary; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/SwiftHaskell/include"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/ghc/include"; }; name = Debug; }; @@ -407,7 +420,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; @@ -418,10 +430,16 @@ INFOPLIST_FILE = SwiftAppLibrary/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = ( + "-undefined", + dynamic_lookup, + ); PRODUCT_BUNDLE_IDENTIFIER = net.nanotechcorp.SwiftAppLibrary; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/SwiftHaskell/include"; SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/ghc/include"; }; name = Release; }; @@ -477,6 +495,7 @@ BFABC4E71E4D781D006036C6 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/SwiftHaskell/include/Main_stub.h b/SwiftHaskell/include/Main_stub.h new file mode 100644 index 0000000..495df3c --- /dev/null +++ b/SwiftHaskell/include/Main_stub.h @@ -0,0 +1,9 @@ +#include "HsFFI.h" +#ifdef __cplusplus +extern "C" { +#endif +extern HsInt32 square(HsInt32 a1); +#ifdef __cplusplus +} +#endif + diff --git a/SwiftHaskell/include/module.modulemap b/SwiftHaskell/include/module.modulemap new file mode 100644 index 0000000..676e18c --- /dev/null +++ b/SwiftHaskell/include/module.modulemap @@ -0,0 +1,4 @@ +module SwiftHaskell { + header "Main_stub.h" + export * +} diff --git a/SwiftHaskellLibrary.cabal b/SwiftHaskellLibrary.cabal index c13783a..e8aa674 100644 --- a/SwiftHaskellLibrary.cabal +++ b/SwiftHaskellLibrary.cabal @@ -21,8 +21,7 @@ executable SwiftHaskell default-language: Haskell2010 hs-source-dirs: src main-is: Main.hs - other-modules: Lib - ghc-options: -threaded -framework-path . + ghc-options: -threaded -framework-path build ld-options: -rpath @executable_path/../Frameworks frameworks: SwiftAppLibrary build-depends: base >= 4.7 && < 5 diff --git a/link-deps.sh b/link-deps.sh new file mode 100755 index 0000000..969cb28 --- /dev/null +++ b/link-deps.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -eu + +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 from a more +# convenient place for Xcode to find. +mkdir -p build/ghc +ln -sf "${GHC_LIB_DIR}/include" build/ghc/ + +# Symlink to the Haskell executable for Xcode. +ln -sf "../${DIST_DIR}/build/${EXECUTABLE_NAME}/${EXECUTABLE_NAME}" build/ diff --git a/src/Main.hs b/src/Main.hs index 74c18d6..e7132c1 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,15 +1,15 @@ -module Main (main) where +module Main where -import Foreign import Foreign.C -import Lib +foreign export ccall square :: CInt -> CInt -foreign import ccall "swiftAppMain" swiftAppMain :: FunPtr (CInt -> CInt) -> IO () -foreign import ccall "wrapper" wrapSquare :: (CInt -> CInt) -> IO (FunPtr (CInt -> CInt)) +square :: CInt -> CInt +square x = x * x + +foreign import ccall "runNSApplication" runNSApplication :: IO () main :: IO () main = do - putStrLn "Hello" - cSquare <- wrapSquare square - swiftAppMain cSquare + putStrLn "hello world" + runNSApplication diff --git a/symlink_deps.sh b/symlink_deps.sh deleted file mode 100755 index 485bb1d..0000000 --- a/symlink_deps.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -eu - -DIST_DIR="$(stack path --dist-dir)" - -ln -sf ../"$DIST_DIR"/build/SwiftHaskell/SwiftHaskell build/ diff --git a/tutorial/empty-app.png b/tutorial/empty-app.png new file mode 100644 index 0000000..7c040ea Binary files /dev/null and b/tutorial/empty-app.png differ diff --git a/tutorial/squared.png b/tutorial/squared.png new file mode 100644 index 0000000..9f058ff Binary files /dev/null and b/tutorial/squared.png differ diff --git a/tutorial/xcode-add-dylib.png b/tutorial/xcode-add-dylib.png new file mode 100644 index 0000000..0aba5f2 Binary files /dev/null and b/tutorial/xcode-add-dylib.png differ diff --git a/tutorial/xcode-bridging-header.png b/tutorial/xcode-bridging-header.png new file mode 100644 index 0000000..0d9300d Binary files /dev/null and b/tutorial/xcode-bridging-header.png differ diff --git a/tutorial/xcode-cocoa-template.png b/tutorial/xcode-cocoa-template.png new file mode 100644 index 0000000..7c8ae79 Binary files /dev/null and b/tutorial/xcode-cocoa-template.png differ diff --git a/tutorial/xcode-copy-files-swifthaskell-executable.png b/tutorial/xcode-copy-files-swifthaskell-executable.png new file mode 100644 index 0000000..ff1c45b Binary files /dev/null and b/tutorial/xcode-copy-files-swifthaskell-executable.png differ diff --git a/tutorial/xcode-copy-frameworks.png b/tutorial/xcode-copy-frameworks.png new file mode 100644 index 0000000..053dd69 Binary files /dev/null and b/tutorial/xcode-copy-frameworks.png differ diff --git a/tutorial/xcode-create-project.png b/tutorial/xcode-create-project.png new file mode 100644 index 0000000..efe36c2 Binary files /dev/null and b/tutorial/xcode-create-project.png differ diff --git a/tutorial/xcode-files-swifthaskell-executable.png b/tutorial/xcode-files-swifthaskell-executable.png new file mode 100644 index 0000000..2cd89ab Binary files /dev/null and b/tutorial/xcode-files-swifthaskell-executable.png differ diff --git a/tutorial/xcode-files.png b/tutorial/xcode-files.png new file mode 100644 index 0000000..92696b4 Binary files /dev/null and b/tutorial/xcode-files.png differ diff --git a/tutorial/xcode-header-search-paths.png b/tutorial/xcode-header-search-paths.png new file mode 100644 index 0000000..f82a986 Binary files /dev/null and b/tutorial/xcode-header-search-paths.png differ diff --git a/tutorial/xcode-new-copy-files-phase.png b/tutorial/xcode-new-copy-files-phase.png new file mode 100644 index 0000000..98232ad Binary files /dev/null and b/tutorial/xcode-new-copy-files-phase.png differ diff --git a/tutorial/xcode-swift-module-search-paths.png b/tutorial/xcode-swift-module-search-paths.png new file mode 100644 index 0000000..9ab15ef Binary files /dev/null and b/tutorial/xcode-swift-module-search-paths.png differ diff --git a/tutorial/xcode-target-membership.png b/tutorial/xcode-target-membership.png new file mode 100644 index 0000000..24add56 Binary files /dev/null and b/tutorial/xcode-target-membership.png differ