这是本机开发深入指南的第一部分将Haskell与Swift和快速用户界面。这是一系列博客文章中的第一篇,涵盖了设置需要使用SwiftUI从XCode项目中的Swift调用Haskell函数。在本系列的后续部分中,我打算至少讨论调用带有惯用Haskell类型和Swift类型的函数(有和没有编组)、SwiftUI观察和iOS开发,这需要GHC为iOS编译目标生成代码。
在撰写本文时,我使用的是XCode 15、Cabal 3.10和GHC 9.8。会的我使用的某些功能仅在这些最新版本中可用,然而,Haskell和Swift之间的互操作性的总体思想不管怎样,现在是7年古老的迅捷-实用仍然具有类似的相关性,并且信息丰富我的方法,尽管最终结果大不相同。
最终目标是创建一个多(苹果)平台应用程序,其UI是使用SwiftUI在Swift中编程,而应用程序的数据和逻辑在从Swift调用的Haskell中实现。
该系列博客文章还附带了一个github存储库,其中每个提交都与本教程的一个步骤相匹配。如果对任何步骤有疑问,只需检查匹配的提交以获得绝对的信心正确理解实际步骤。访问haskell-x-swift-project-steps存储库的链接!如果时间允许,我还打算录制一段视频解释。
此摘要已交叉发布到罗德里戈的博客.
你好,斯威夫特,我是哈斯克尔!
在这一部分中,我们只关心你好,世界!
去。
- 我们将设置一个导出函数的Haskell(外部)库
hs_系数
那个使用C FFI返回整数的阶乘
- 设置调用
hs_系数
- 将Haskell代码编译到共享库中
- 创建Swift模块
Haskell框架
导出Haskell函数(从存根C头文件导入),并根据Haskell共享库。
- 导入
Haskell框架
进入SwiftUI应用程序以成功呼叫hs_系数
并在运行的屏幕上显示结果应用程序。
下图(由呈现对角)描述Swift可执行文件和Haskell库将从非远距的角度进行连接。可能是这样在整个帖子中偶尔参考一下这个图表很有用!
┌───────────────┐┌───────────┐┌───────────────────────┐┌───────────┐│Haskell库││cbits│Дgen-dynamic-settings.sh││的RTS头│└┬──────────────┘└┬─────────┬┘└─────────────┬─────────┘└┬──────────┘┌▽────────────────▽───────┐┌▽──────────────┐│ ││Haskell外国图书馆││页眉(cbits)││ │└┬───────────────────────┬┘└─────────────┬─┘│ │┌▽─────────────────────┐┌▽──────────────┐│ │ ││共享动态库││标头(存根)││ │ │└┬────────────────────┬┘└┬──────────────┘│ │ │©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©│ │┌──────│──┘ │ ┌─────────┘│┌───────────▽▽┐┌────▽─────────────────────▽─▽┐││Clang模块││DynamicBuildSettings.xcconfig││└┬────────────┘└┬────────────────────────────┘│ │┌─────────────▽────────┐│ ││BuildSettings.xcconfig││ │└┬─────────────────────┘┌▽─▽─▽──────┐│SwiftUI应用程序└───────────┘
设置SwiftUI应用程序
让我们使用SwiftUI作为主界面来设置一个简单的XCode项目。火灾启动XCode并创建一个macOS应用程序,名为斯威夫特·哈斯克尔
,使用SwiftUI,不包括测试。选择个人团队而不是无-你可能必须这样做创建一个(免费)。
在新创建的项目中,应该存在两个文件:SwiftHaskellApp.swift银行
和ContentView.swift目录视图
.我们可以马上改变ContentView.swift目录视图
显示调用结果hs_系数(5)
尽管如此hs_系数
尚不在范围内:
进口 快速用户界面
结构内容视图以下为: 查看 {
无功功率,无功功率 身体以下为:一些视图{
V堆栈{
文本(“哈斯克尔,你好:\(hs_系数(5)!")
}
.衬垫()
}
}
在继续Haskell端之前,创建一个新建文件>配置设置文件
(也称为.xcconfig文件
文件)命名构建设置.xcconfig
。我们将使用此文件写入所有生成设置而不是使用XCode的构建设置导航器。
使用我们的.xcconfig文件
项目设置的文件,位于信息>配置
在“项目”选项卡中,选择生成设置
文件。对于要在XCode中显示的配置.xconfig(.xconfig)
一定在树上navigator(如果在XCode中创建模块,则默认情况下会发生)。您可以阅读更多内容,或了解如何设置.xconfig(.xconfig)
文件作为配置,在此写入xcconfig配置
由NSHipster提供。
尽管我们正在设置.xcconfig文件
手动归档(以及例如。初始化XCode项目),可以使用使用所谓的XCode项目生成器的编程方法,例如X代码生成和犹太教教徒.
建立哈斯克尔外国图书馆
创建文件夹哈斯克尔框架
在XCode项目中,光盘
融入其中,并且从那里跟随。
我们直接跳进了一个成熟的哈斯克尔计划与阴谋集团合作,其中,我们使用外国图书馆
节。
从带有图书馆
暴露的节我的图书馆
、和添加函数hs_系数
到我的图书馆
在上运行的CInt公司
秒:
模块 我的图书馆 哪里
进口 国外。C类
hs_系数:: CInt公司 -> CInt公司
hs_系数x= 产品[1..x](x)
这里代码的组织并不十分重要。也许在例如,您可能希望只使用C类型,例如CInt公司
在国外的图书馆里。
在cabal文件中,添加外国图书馆
带有
外国的-哈斯克尔图书馆-外国的-框架
类型以下为:本地的-共享
--尽管行为未定义,但这在Mac上应该有效
--请参见https://www.hobson.space/posts/haskell-foreg-library/(读得好)
选项以下为:独立的
--我们将C存根头文件复制到根目录中的一个文件夹中。
--如果你在图书馆有出口报关单
--确保在那里也添加了这个标志(这样所有存根都会被添加
--到“haskell-framework-include”文件夹)
温室气体-选项以下为: -stubdir公司=哈斯克尔-框架-包括
其他-模块以下为: 我的外国图书馆
建造-取决于以下为:底座,haskell-框架
小时-来源-目录以下为:飞行
不幸的是,选项:独立
仅得到官方支持(和需要),即使它正是我们所需要的。然而,非正式地,macOS发行版应该能够安全地使用此选项–有关更多信息,请参阅关于外国图书馆的综述,解释了此选项的原因macOS未定义.在未来,这可能是开箱即用的,而且没有定义行为,或者macOS上的行为可能已经改变有效……但让我们期待前者。此外,我们通过了-stubdir公司
供GHC将C存根头文件输出到目录哈斯克尔框架工程
。请添加自动生成的目录到.gitignore(.git忽略)
.
创建文件flib/MyForeignLib.hs
声明了对外出口
属于hs_系数
从导入我的图书馆
和对外出口
是这样的:
模块 我的外国图书馆 哪里
进口 国外。C类
进口 我的图书馆(hs_系数)
国外出口商品分类hs_系数:: CInt公司 -> CInt公司
似乎没有从中重新导出函数我的图书馆
当它是外国的从那里导出的内容足以包含在共享库中(可能是一个错误),我们确实需要对外出口
在这里而不是在里面我的图书馆
.
正在运行阴谋集团建造
现在应该生成一个哈斯克尔框架工程
文件夹,其中包含我的外国图书馆。小时
、和libhaskell-foreview-framework.dylib(libhaskel-foreview-framework-dylib)
共享库在某处dist新闻风格
(你可以找到-名称libhaskell-foreview-framework.dylib
找到它)
我们将对此库测试C程序,以检查它是否按预期工作。创建脚本/test-haskell-forewin-lib.sh
使用编译C中的main函数调用hs_系数
.一些注意事项:
- 我们需要传递到构建的共享库的路径(
$HS_FLIB_PATH(美元)
)到编译器。
- 我们需要将路径传递到标题(
$HS_HEADERS_PATH(美元)
).
- 我们将共享库的路径硬编码为
rpath(rpath)
搜索路径(仅用于测试目的)。在构建macOS应用程序时,XCode将添加@可执行路径//框架
到rpath(rpath)
搜索路径,因此我们可以简单地将共享库复制到基于应用程序的位置(框架
).
- 我们需要打电话
hs_输入
和hs_退出
初始化运行时系统(请参见相关GHC用户指南部分).
- 我们需要使用
温室气体
,因为它会自动包含并链接rts标头和库。使用C编译器我们还需要找到Haskell的rts头和库安装。
#!/usr/bin/env-bash
设置 -e(电子)
如果 !测试 -(f) “haskell-framework.cabal”; 然后
回声 “从项目根目录运行此脚本!”
出口1
菲
HS_FLIB_PATH(高速库路径)=$(目录名 $(找到.-名称libhaskell-foreview-framework.dylib(libhaskel-foreview-framework-dylib)))
HS_航向_PATH=哈斯克尔框架工程
回声 "
#包括<stdio.h>
#包括<MyForeignLib_stub.h>
#包括<HsFFI.h>
int main(无效){
hs_init(空,空);
打印(\"%d\n天\",hs_系数(5));
hs_退出();
返回0;
}
" >conftestmain公司。c(c)
#我们使用“ghc”而不是“gcc”,因为否则我们还需要提供
#运行时系统(Rts)的include和lib路径
温室气体 -无hs-主管道 -o(o)conftest-conftestmain公司。c(c)\
-lhaskell-foreng-framework公司 \
-我"$HS_HEADERS_PATH(美元)" \
-L(左)"$HS_FLIB_PATH(美元)" \
-optl-Wl,-rpath,"$HS_FLIB_PATH($HS_FLIB_PATH)"
结果=$(./conftest公司)
如果 [120-等式 $结果 ]; 然后
回声 “成功调用外国图书馆!”
其他的
回声 “糟糕的外国图书馆!”
出口1
fi(菲涅耳)
rm(毫米) -(f)conftest公司*
你应该得到成功调用外国图书馆!
当运行此脚本时。
将Haskell库与可执行文件链接
我们在Swift中调用国外导出的Haskell函数的方法:
- 创建Swift模块通过模块映射导出Haskell函数指向导出Haskell函数的标头。
- 扩展模块搜索路径带有新模块地图的位置。
- 将该模块作为SwiftUI代码中的模块导入,并使用所需的函数。
- 在链接时,带有程序使用的符号的共享库必须链接到,并且必须在运行路径可以通过复制来完成将共享库绑定到应用程序中
框架
文件夹。
我们创建一个模块映射文件,列出导出Haskell函数的所有头定义Haskell函数所在的Swift模块,使用Clang的模块系统。模块映射类似于
模块 Haskell框架{
收割台“haskell-framework/haskell-framewort-include/MyForeignLib.h”
出口*
}
并可以使用导入Swift代码导入HaskellFramework
,只要模块图可用作模块.模块映射
在中导入搜索路径.正如人们所料,导入此模块会将导出的所有名称都纳入范围从列出的标头。
具体来说,我们将使用推断子模创建模块映射的模块功能。利用推断出的子模,我们可以只需定义雨伞目录,并为每个目录获取子模块该目录中的标头(任意嵌套,其中标头A/B/C.小时
成为命名的子模块主模块。A.B.C公司
)
在XCode项目的根目录中,编写一个模块.模块映射
文件:
模块 Haskell框架{
雨伞“haskell框架/haskell框架包含”
明确的模块 *{
出口*
}
}
这个雨伞
关键字指定查找头文件的目录对于我们的子模块显式模块*
线条是推断的子模块部分,因为每个标头将导致一个大致如下的声明显式模块HeaderName{header“伞形/HeaderName.h”…}
.实际上,我们上面的模块图将扩展到:
模块 Haskell框架{
明确的模块 我的外国图书馆{
收割台“haskell-framework/haskell-framewort-include/MyForeignLib.h”
出口*
}
}
同样,要明确的是,这是我们最初的模块.模块映射
使用雨伞
关键字当前扩展为,不是我们写的文件.
写了我们的模块.模块映射
,我们需要扩展编译器的进口搜索路径查找此模块图。因为我们已经设置好了xcconfig配置
-基于配置,这相当于写入构建设置.xcconfig
以下为:
SWIFT_INCLUDE_PATHS银行代码=$(项目_DIR)
这相当于更改Swift编译器-搜索路径>导入路径
在XCode中构建设置(实际上,通过检查最右边的检查器面板,您将找到相应的xcconfig配置
名字确实是SWIFT_INCLUDE_path(包含路径)
–这也在xcconfig配置
文章).
返回内容视图.swift
,其中hs_系数
正在被呼叫,您应能够在文件顶部添加,并使XCode成功识别:
即使导入被识别,它也无法成功编译。这个原因是我们的存根头(我的外国图书馆。小时
)包括<HsFFI.h>
哪一个XCode找不到。我们需要扩展我们的标题搜索路径使用RTS标头的路径。
目前,我们的构建设置.xcconfig
只能包含静态已知信息。幸运的是,我们可以#包括
其他xcconfig配置
文件(可能已动态生成)构建设置.xcconfig
(如所述由xcconfig配置
书面记录).我们通过在构建设置.xcconfig
文件:
#包括“DynamicBuildSettings.xcconfig”
我们将生成动态构建设置.xcconfig
使用脚本haskell-framework/scripts/gen-dynamic-settings.sh
它调用
找出现有GHC安装的rts包含路径。
我们扩展标题_搜索_路径
,的xcconfig配置
列出路径的变量XCode将在生成时使用RTS标头的路径搜索标头文件夹:
#!/usr/bin/env-bash
设置 -e(电子)
如果 !测试 -(f) “haskell-framework/haskell-framework.cabal”; 然后
回声 “从XCode项目的根目录运行此脚本!”
出口1
fi(菲涅耳)
回声 "
标题_搜索_路径=\$(继承)$(ghc-pkg字段rts包含dirs--简单输出 | 信托收据 ' ' “\n” | 尾 -n1个)
" >动态构建设置.xcconfig
回声 “已创建DynamicBuildSettings.xcconfig!”
确实要添加动态构建设置xcconfig
到.gitignore(.git忽略)
.文字字符串$(继承)
是xcconfig配置
继承选项的语法在应用此配置之前设置。此外,要求包括直径
属于实时
输出两个目录:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/ffi
/用户/romes/.ghcup/ghc/9.8.1/lib/ghc-9.8.1/lab//lib/arch64-osx-ghc-9.8.1/rts-1.0.2/包括
然而飞行情报员
默认情况下,标头已包含在XCode中的模块中应用程序,所以我们需要将其从搜索路径中删除,以避免模块“外国金融机构”的重新定义
错误(信托收据
与尾部-n1
只选择RTS标头的路径)。
现在XCode可以找到该函数,因为它是在编译中定义的模块成功并将功能纳入范围。然而,构建程序将失败链路时间错误:尽管我们指示编译器查找Haskell的定义我们想要使用的函数以及导出它们的模块,我们还没有链接到定义实际符号的库。
上一节中创建的Haskell外部库编译为共享动态库。在构建Swift应用程序时链接它我们需要通过-lhaskell-foreng-framework公司
编译工具链和告诉它在哪里可以找到这个图书馆。第一步可以分为两步兼容(因为两者可以共存)方式:
- 添加
链接“haskell-foreng-framework”
模块映射的声明(已解释在这里)
- 有一条关于此功能的注释尚未在然而,链接我机器中的库是可行的使用XCode 15。
- 添加
-lhaskell-foreng-framework公司
向其他_堵塞
建造在中设置构建设置.xcconfig
。即使你也可以这样做使用了link指令。
添加后链接
声明,您的模块.模块映射
应包含:
模块 Haskell框架{
雨伞“haskell框架/haskell框架包含”
明确的模块 *{
出口*
}
链接“haskell-foreng-framework”
}
其次,我们需要将共享库路径添加到库搜索路径并通过将其复制到框架
文件夹与应用程序捆绑在一起。通过将库复制到此文件夹,我们确保在动态时可以找到它在运行时加载:库安装名称与@rpath(rpath)
,即它是一依赖于运行路径的库,并且在中搜索XCode构建的可执行文件的运行路径依赖项Frameworks文件夹,相对于可执行文件路径(@可执行路径//框架
).
依赖于运行路径的库是其完整安装的依赖库创建库时名称未知(请参见动态库使用)。相反,库指定动态加载程序必须解析加载依赖于库。
要使用依赖于运行路径的库,可执行文件提供运行路径列表搜索路径,动态加载程序在加载时遍历这些路径以查找库。
实际上,我们通过扩展库搜索路径
设置动态地添加“复制”构建阶段,将共享库复制到列出了Frameworks文件夹。此时,我不知道如何复制在XCode之外–如果你知道怎么做,一定要给我发短信。这也是不幸的我们必须在那里硬编码动态库的路径,而不是在构建时计算它。
通过在哈斯克尔框架
目录:
阴谋集团列表-bin haskell-foreng-framework
然后,在项目设置的“构建阶段”选项卡下,添加(通过单击小加号)a新建复制文件阶段
。然后,单击的加号要复制的文件的新列表,添加haskell外部框架.dylib公司
(共享库),位于通过运行上面的通过单击“添加其他”命令。
致haskell-framework/scripts/gen-dynamic-settings.sh
,添加以下内容回显到文件之前的行
推送的.>/开发/空
光盘哈斯克尔框架
FLIB_PATH(飞行航路)=$(阴谋集团list-bin haskell-foreng-框架)
邻苯二胺 >/开发/空
以及所写的内容动态构建设置.xcconfig
添加以下行
库_搜索_路径=\$(继承) $(目录名 $FLIB_PATH)
此时,在重新生成动态构建设置之后,您应该能够以成功链接应用程序并运行它。
RTS必须初始化
惊喜!运行应用程序将在运行时失败hs_系数
是打电话。要从用另一种语言编写的可执行文件调用Haskell函数,必须首先初始化GHC运行时系统,并在以下情况下终止它适当的。我们需要调用函数hs_输入
和结束(_E)
,暴露于香港金融金融机构。小时
。我们将在我们的外部库中编写两个包装器函数来调用相反,如GHC用户指南中的FFI章节.
我们创建了一个哥伦比亚广播公司
文件夹中的哈斯克尔框架
Haskell项目将我们的C文件和标头,并将它们添加到外国图书馆
阴谋集团的诗节文件:
包括直径:哥伦比亚广播公司
c来源:cbits/MyForeignLibRts。c(c)
安装包括:我的外国图书馆。小时
您可以看到这些选项在中的作用本集团用户指南部分.我们创造cbits/MyForeignLibRts。c(c)
将调用包装到hs_输入
和hs_结束
如上文链接的外国金融机构章节所述:
#包括<标准库.h>
#包括<标准o.h>
#包括<HsFFI.h>
HsBool flib_init() {
打印(“初始化flib\n个");
//初始化Haskell运行时
hs_输入(无效的,无效的);
//在此处进行其他库初始化
返回HS_BOOL_真;
}
空隙flib_end(飞行_结束)() {
打印(“正在终止flib\n个");
退出(_E)();
}
看起来你可以国外进口
这些函数进入Haskell库并使用重新导出对外出口
但是,如果它们被导出哈斯克尔表示,他们自己要求有效地初始化RTS破坏了RTS初始化功能的目的。因此,我们编写一个与库一起提供的头文件,以便它包含在Swift项目。文件cbits/MyForeignLibRts。小时
包含:
#包括<HsFFI.h>
HsBool flib_init();
空隙flib_end(飞行_结束)();
回到Swift端,我们需要用模块映射来增加模块映射到RTS初始化包装标头。我们添加了第二个子模块声明:
明确的模块 RTS管理{
收割台“haskell-framework/cbits/MyForeignLibRts.h”
}
这个cbits/MyForeignLibRts。c(c)
符号将包含在共享动态中库。
您可以使用脚本./build-haskell
在XCode项目的根目录中:
#!/usr/bin/env bash(usr/bin/env bash)
设置 -e(电子)
如果 !测试 -d日 “SwiftHaskell.xcodeproj”; 然后
回声 “从SwiftHaskell XCode项目根目录运行此命令!”
出口1
fi(菲涅耳)
推送的.>/开发/空
光盘哈斯克尔框架/
阴谋集团全部生成--水壶
./scripts/test-haskell-foreign-lib.sh
邻苯二胺 >/开发/空
./haskell-framework/scripts/gen-dynamic-settings.sh
回声 “完成。”
最后,在SwiftHaskellApp.swift银行
,我们扩展了@主要
应用程序
通过重写这个初始化()
函数:调用flib_init()
初始化运行时系统并设置观察员呼叫flib_end()
在以下情况下结束运行时系统应用程序终止。我们只需要进口Haskell框架.RTS管理
到将这些功能纳入范围:
@主要
结构SwiftHaskell应用程序以下为: 应用程序 {
初始化() {
flib_init(飞行初始化)()
通知中心.违约.添加观察员(用于名称以下为:NS应用程序.将终止通知,对象以下为: 无,队列以下为: .主要的) {_在里面
//终止
flib_end(飞行_结束)()
}
}
...
}
运行应用程序应该可以正常运行并自豪地打印120
在屏幕上。
本系列博客文章的第一部分已经结束。接下来是通信更有趣的数据类型(有和无编组),使东西更符合人体工程学,SwiftUI观察,iOS编译,并可能开发一个简单的模型应用程序。
这个哈斯克尔-x-swift-project-stepsgit存储库有一个与本指南中每个步骤相匹配的提交,因此如果如果有任何不清楚的地方,您可以在检查时让代码自己说话提交。
进一步阅读