Open33

macOS (Apple Silicon) で GHC の WASM バックエンドを使ってみる実験

konnkonn

自分で WASM Backend をビルドしてもいいが、せっかく GHCup で cross compiler の Linux 向け prerelease が容易されているので、これを使って Devcontainer で楽をできないかやってみる。Docker エンジンには、ARM なので、あるていど Intel コンテナも速いらしい Orbstackを使ってみる。

追記:成果物は以下のレポジトリに蓄積していく:

https://github.com/konn/ghc-wasm-earthly

konnkonn

とりあえず Ubuntu をベースにする。

konnkonn

Create Dev Container... で作成をすると案の定 aarch64 になっている。

$ arch 
aarch64

たぶん GHCup で配付されているものは aarch64 はサポートしていないはず。要確認。

konnkonn

とりあえずデフォの状態で以下を実行してみる:

$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
$ ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-prereleases-0.0.7.yaml
$ ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-cross-0.0.8.yaml
$ ghcup install ghc wasm32-wasi-9.10.0.20240412
[ Warn  ] New ghc version available. If you want to install this latest version, run 'ghcup install ghc 9.8.2'
[ Error ] [GHCup-00010] Unable to find a download for GHC version 'wasm32-wasi-9.10.0.20240412' on detected platform aarch64-linux-ubuntu-22.04
[ Error ] Also check the logs in /home/vscode/.ghcup/logs

やっぱり aarch には来てない。Intel たてよう。

konnkonn

デフォルトの devcontainer.json

.devcontainer/devcontainer.json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
{
	"name": "Ubuntu",
	// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
	"image": "mcr.microsoft.com/devcontainers/base:jammy"

	// Features to add to the dev container. More info: https://containers.dev/features.
	// "features": {},

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "uname -a",

	// Configure tool-specific properties.
	// "customizations": {},

	// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
	// "remoteUser": "root"
}
konnkonn

devcontainer の Dockerfile を雑に書いたらビルドに失敗してしまった。これローカルのどこに Dockerfile あるんだっけ

konnkonn

こんな感じにしてみたが、ghcup での wasm backend のインストールに失敗している。

.devcontainer/Dockerfile
FROM simonwhitaker/gibo AS gibo


FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/base:jammy

RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_MINIMAL=1 sh
RUN pwd
RUN ls ~/
ENV PATH=${PATH}:/root/.local/bin
ENV PATH=${PATH}:/root/.ghcup/bin

RUN apt-get update && \
    apt-get -y install --no-install-recommends git sudo jq bc make automake rsync htop curl build-essential lsb-release pkg-config libffi-dev libgmp-dev software-properties-common libssl-dev libtinfo-dev libsystemd-dev zlib1g-dev make g++ wget libncursesw5 libtool autoconf && \
    apt-get clean

RUN ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-prereleases-0.0.8.yaml
RUN ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-cross-0.0.8.yaml
RUN ghcup install cabal 3.11.0.0.2024.4.19
RUN ghcup install ghc wasm32-wasi-9.10.0.20240412

COPY --from=gibo /gibo /usr/local/bin/gibo
.devcontainer/devcontainer.json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
{
  "name": "GHC 9.10 WASM Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },

  // Features to add to the dev container. More info: https://containers.dev/features.
  // "features": {},

  // Use 'forwardPorts' to make a list of ports inside the container available locally.
  // "forwardPorts": [],

  // Use 'postCreateCommand' to run commands after the container is created.
  // "postCreateCommand": "uname -a",

  // Configure tool-specific properties.
  "customizations": {
    "vscode": {
      "extensions": [
        "haskell.haskell",
        "mads-hartmann.bash-ide-vscode",
        "castello-dev.bash-snippets",
        "arahata.linter-actionlint",
        "chunsen.bracket-select",
        "mindaro-dev.file-downloader",
        "mkxml.vscode-filesize",
        "vinirossa.vscode-gitandgithub-pack",
        "github.vscode-github-actions",
        "GitHub.copilot",
        "GitHub.vscode-pull-request-github",
        "GitHub.remotehub",
        "eamodio.gitlens",
        "oderwat.indent-rainbow",
        "pascalsenn.keyboard-quickfix",
        "yzhang.markdown-all-in-one",
        "DavidAnson.vscode-markdownlint",
        "christian-kohler.path-intellisense",
        "ionutvmi.path-autocomplete",
        "earshinov.permute-lines",
        "mutantdino.resourcemonitor",
        "emeraldwalk.RunOnSave",
        "timonwong.shellcheck",
        "wayou.vscode-todo-highlight",
        "DeepInThought.vscode-shell-snippets",
        "Swellaby.workspace-config-plus"
      ],
      "settings": {
        "terminal.integrated.defaultProfile.linux": "/usr/bin/zsh"
      }
    }
  }

  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
  // "remoteUser": "root"
}
konnkonn

コンテナ内で直接実行してみるとこんなエラーである:

$ ghcup install ghc wasm32-wasi-9.10.0.20240412
[ Info  ] downloading: https://github.com/amesgen/ghc-wasm-bindists/releases/download/20240414T232345/wasm32-wasi-ghc-9.10.tar.xz as file /root/.ghcup/tmp/ghcup-62532b224a424835/ghc-9.10.0.20240412-x86_64-linux-alpine3_18-wasm-cross_wasm32-wasi-release+fully_static.tar.xz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  291M  100  291M    0     0  41.6M      0  0:00:06  0:00:06 --:--:-- 45.9M
[ Info  ] verifying digest of: ghc-9.10.0.20240412-x86_64-linux-alpine3_18-wasm-cross_wasm32-wasi-release+fully_static.tar.xz
[ Info  ] Unpacking: ghc-9.10.0.20240412-x86_64-linux-alpine3_18-wasm-cross_wasm32-wasi-release+fully_static.tar.xz to /root/.ghcup/tmp/ghcup-ac1be7cefad993e2
[ Info  ] Installing GHC (this may take a while)
[ ghc-configure ] checking for unistd.h... yes
[ ghc-configure ] checking size of void *... 8
[ ghc-configure ] configure: error: This binary distribution produces binaries for a target with
[ ghc-configure ]                   word size of 4, but your target toolchain produces binaries
[ ghc-configure ]                   with a word size of 8. Are you sure your toolchain
[ ghc-configure ]                   targets the intended target platform of this compiler?
[ Error ] [GHCup-00841] Process "sh" with arguments ["./configure",
[ ...   ]                              "--prefix=/root/.ghcup/ghc/wasm32-wasi-9.10.0.20240412",
[ ...   ]                              "--target=wasm32-wasi"] failed with exit code 1.
[ Error ] Also check the logs in /root/.ghcup/logs
konnkonn

これで試し中(Intel on ARM なのでさすがにinstall に数分かかる)

.devcontainer/Dockerfile
FROM simonwhitaker/gibo AS gibo


FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/base:jammy

RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_MINIMAL=1 sh
RUN pwd
RUN ls ~/
ENV PATH=${PATH}:/root/.local/bin
ENV PATH=${PATH}:/root/.ghcup/bin

RUN apt-get update && \
    apt-get -y install --no-install-recommends git sudo jq bc make automake rsync htop curl build-essential lsb-release pkg-config libffi-dev libgmp-dev software-properties-common libssl-dev libtinfo-dev libsystemd-dev zlib1g-dev make g++ wget libncursesw5 libtool autoconf && \
    apt-get clean

RUN ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-prereleases-0.0.8.yaml
RUN ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-cross-0.0.8.yaml
RUN ghcup install cabal 3.11.0.0.2024.4.19

# Sets-up GHC WASM32 backend
RUN apt-get -y install --no-install-recommends zstd
RUN \
  git clone https://gitlab.haskell.org/ghc/ghc-wasm-meta.git && \
  cd ghc-wasm-meta && \
  bash setup.sh && \
  source ~/.ghc-wasm/env && \
  ghcup install ghc wasm32-wasi-9.10.0.20240412 -- \
    --host=x86_64-linux --with-intree-gmp --with-system-libffi

COPY --from=gibo /gibo /usr/local/bin/gibo
konnkonn

ウオー5分くらいかかった挙句失敗した…… sh だから source がないんだな。以下のようにしてリトライ:

.devcontainer/Dockerfile
FROM simonwhitaker/gibo AS gibo


FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/base:jammy

RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_MINIMAL=1 sh
RUN pwd
RUN ls ~/
ENV PATH=${PATH}:/root/.local/bin
ENV PATH=${PATH}:/root/.ghcup/bin

RUN apt-get update && \
    apt-get -y install --no-install-recommends git sudo jq bc make automake rsync htop curl build-essential lsb-release pkg-config libffi-dev libgmp-dev software-properties-common libssl-dev libtinfo-dev libsystemd-dev zlib1g-dev make g++ wget libncursesw5 libtool autoconf && \
    apt-get clean

RUN ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-prereleases-0.0.8.yaml
RUN ghcup config add-release-channel https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-cross-0.0.8.yaml
RUN ghcup install cabal 3.11.0.0.2024.4.19

# Sets-up GHC WASM32 backend
RUN apt-get -y install --no-install-recommends zstd
RUN \
  git clone https://gitlab.haskell.org/ghc/ghc-wasm-meta.git && \
  cd ghc-wasm-meta && \
  bash setup.sh && \
  . ~/.ghc-wasm/env && \
  ghcup install ghc wasm32-wasi-9.10.0.20240412 -- \
    --host=x86_64-linux --with-intree-gmp --with-system-libffi

COPY --from=gibo /gibo /usr/local/bin/gibo
konnkonn

10分でビルド完了した。ただ、root ユーザになってしまっているが、VSCode で開くと今度は vscode というユーザ名になってしまう。このあたり絶対良く知られた対処法があるはずなので探してみよう。

konnkonn

あと何もしてなくても Orbastack が全 CPU 使っている。ちょっと設定をいじることにする。CPU limit を50 % くらいに。

konnkonn

面倒なので一旦 "remoteUser": "root", を devcontainer.json に足す。そうするといいかんじに実行ファイル群はみつかってくれる。

$ wasm32-wasi-ghc-9.10
wasm32-wasi-ghc-9.10.0.20240412: no input files
Usage: For basic information, try the `--help' option.

とはいえ root にするのはなんかやなので、 remoteUser をちゃんと vscode にしてインストールする方向にする(また15分待ちか……)

konnkonn

遂に動いた!!!!

$ cat hello.hs
module Main where

main :: IO ()
main = putStrLn "Hello, WASM World from GHC 9.10!"

$ wasm32-wasi-ghc --make hello.hs
[1 of 2] Compiling Main             ( hello.hs, hello.o )
[2 of 2] Linking hello.wasm

$ wasmtime ./hello.wasm
Hello, WASM World from GHC 9.10!
konnkonn

Devcontainer はじめてつかってどちゃんと gpg forward されてるっぽいのうれしすぎるな

konnkonn

なんか extension がちゃんとインストールされてないな。これは devcontainer の固有のことだと思うけどなんなんだろう

konnkonn

HLS とか使いたいなと思ったが、wasm32-ghc は GHCi をサポートしていないようで、HLS は武装強化された GHCi みたいなものだから、あんまり devcontainersにする意味はなかったかもしれない。方針としては、イメージのビルドだけ作った Docker image 内でやり、残りは手元に対応する GHC 9.10 alpha (通常版) を入れて、HLS をそれに向けてビルドする、みたいなのが現実的かもしれない。
あと、WASM 向けのライブラリである GHC.Wasm.Prim とか Wasm の JS FFI については普通の GHC ではサポートされていないので(GHC.Wasm.Prim が通常版にないのは確認できているが JS FFI については確証なし)、そのへんのコンパチをつくるところからやるべきかな

konnkonn

フラグや Hackage HEAD を使って色々細工しても、現状の HLS は GHC 9.10-alpha でビルドできないようだ(GHC API の非互換にやられる)。まあパッチすれば素の ghcide 部分くらいはつかえるだろうが、それするくらいなら手元では GHC 9.8 を使う、くらいで良い気がしてきた

konnkonn

Dockerfile をルートに移した上で、Earthly で WASM をビルドするようにした。

Earthfile
VERSION 0.8
FROM DOCKERFILE --platform=linux/amd64 -f ./Dockerfile -
WORKDIR /workdir

hello:
  COPY --keep-ts . .
  RUN env
  RUN cat /root/.ghc-wasm/env
  RUN wasm32-wasi-ghc --make hello.hs
  SAVE ARTIFACT hello.wasm AS LOCAL _build/hello.wasm

いったん HLS は GHC 9.8 のものを使う。

konnkonn

とりあえず、0b9ab8d の時点でこんな感じでやると WASM がビルドできるようになった。
初回実行時はコンテナのビルドに15分ほどかかる。

Earthly さえ入っていれば、以下のようにしてホストの macOS上 に直接 WASM が降ってくるようになる:

$ earthly build +hello
 Init 🚀
————————————————————————————————————————————————————————————————————————————————

           buildkitd | Found buildkit daemon as docker container (earthly-buildkitd)

 Build 🔧
————————————————————————————————————————————————————————————————————————————————
...

$ tree _build
_build
└── hello.wasm

1 directory, 1 file

$ wasmtime  ./_build/hello.wasm
Hello, WASM World from GHC 9.10!
konnkonn

TH で環境に応じて WASM 側では FFI を使って native 側ではダミーを生成したりするぞ!と意気込んだが、WASM backend は TH に対応していない(今後対応予定)のでうまくいかなかった。

konnkonn

JS FFI と相互呼出で での で実行できるところまでいった 🎉

$ earthly +hello-js
...

$ cd ./_build/console-log/

$ deno run --allow-read run.ts console-log.wasm
Hello, world! (cabalised, JS FFI!)