UnityでHaskellのコードを実行する方法の模索


# 概要

Unityでのゲーム制作には基本的にはC#を使う。しかし、場合によっては他の言語を用いたいことがあり、Unityでは[ネイティブプラグイン](https://docs.unity3d.com/ja/2017.4/Manual/NativePlugins.html)という方法で、外部で生成されたDLLを利用することが可能である。
筆者は、ダンジョン生成のプログラムをHaskellで作成し、それをUnity内で利用したいという経緯があったので、今回、HaskellのシンプルなコードをCabalとStackを利用してDLLを作成し、それをUnityから利用する効率的な方法を模索した。

環境はWindows 10 64bit

# 1. 準備

Stackで新しいプロジェクトを作る。

```sh
stack new haskell-foreign-library-test
```

一度ビルド`stack build`して、package.yamlからcabalファイル（haskell-foreign-library-test.cabal）を生成する（バックでhpackが動いている）。

後述するが、ライブラリ作成にはCabalの機能`foreign-library`を使う。しかし、現在（2019/01/04）、cabalファイルを生成するhpackは、これに未対応なようので、泣く泣くpackage.yamlは削除しておく。

hpackのforeign-library対応は、[議論](https://github.com/sol/hpack/issues/258)されているようだ。

ついでに*.gitignore*のcabalの行を削除して、cabalファイルをバージョン管理に含めておきたいところ。

# 2. HaskellプログラムのDLL作成

Haskell側の全てのコードは[ここ（masatoko/haskell-foreign-library-test）](https://github.com/masatoko/haskell-foreign-library-test)で見られる。

## 2.1. Haskellのコード

Haskellのコードは非常にシンプルなものにした。受け取った整数に１を足して返すだけの関数*plus_one*だ。
これを`foreign export`で出力しておく。

```haskell:src/Lib.hs
module Lib where

foreign export ccall plus_one :: Int -> IO Int

plus_one :: Int -> IO Int
plus_one x = return $ x + 1
```

## 2.2. hs_init関数のラッパーをC言語で作成

[参考（8.2. GHCでFFIを使う）](http://www.kotha.net/ghcguide_ja/7.6.2/ffi-ghc.html)

外部から*plus_one*を利用するには、使う前にGHCのランタイムシステムを初期化し、使ってから終了させなければならない。
hs_init() -> plus_one() -> hs_exit()　という流れ。
しかし、hs_init()は引数をとり、外部（C#）から利用する際、引数を渡すのが面倒になるので、hs_init()のラッパー関数*hs_init_wrapper()*を作成しておく。

```c:crsc/mylib.h
#pragma once

extern "C" {
void hs_init_wrapper(void);
}
```

```c:csrc/mylib.cpp
#include "HsFFI.h"
#include "mylib.h"
#include "Lib_stub.h" // 今回は不要。ここでplus_one()を使うことができる。

void hs_init_wrapper(void) {
  int argc = 2;
  char *argv[] = {(char *)"+RTS", (char *)"-A32m", NULL};
  char **pargv = argv;
  hs_init(&argc, &pargv);
}
```

mylib.cppでインクルードされている*Lib_stub.h*は自動生成される。
（*.stack-work/dist/***/build/mylib/mylib-tmp/Lib_stub.h*に出力されていた。）
中身を見てみよう。

```cpp:Lib_stub.h
#include "HsFFI.h"
#ifdef __cplusplus
extern "C" {
#endif
extern HsInt plus_one(HsInt a1);
#ifdef __cplusplus
}
#endif
```

plus_oneがエクスポートされている。
外部からより使いやすくするために、関数を加工して、また別の関数として出力しておくのもいいと思う。

## 2.3. ビルド

`stack build`の１コマンドでDLLを生成してくれたら嬉しい。その方法を調べた。
Cabalはv2.0.0.2（2017年7月）から`foreign-library`でDLL（macの場合はdylib）を作成できるようになったらしい。これを使いたい。[公式の説明](https://www.haskell.org/cabal/users-guide/developing-packages.html?#foreign-libraries)

```cabal:haskell-foreign-library-test.cabal
library
  hs-source-dirs:
      src
  other-modules:
      Lib
  build-depends:
      base >=4.7 && <5
  default-language: Haskell2010

foreign-library mylib
  type:
      native-shared

  if os(Windows)
    options: standalone
  
  other-modules:
      Lib
  build-depends:
      base >=4.7 && <5
  hs-source-dirs:
      src
  c-sources:
      csrc/mylib.cpp
  default-language: Haskell2010
```

これでビルドすればDLLが作成される。

```sh
stack build
```

## 2.4. 生成されたDLLを確認

生成されたDLLのパスは、`stack build`のログの最後のほうに表示されている。
今回は、*.stack-work/install/426b9cbc/lib/mylib.dll*　に生成されていた。

nmコマンドで中身を確認。

```sh
nm mylib.dll
```

以下の行を発見し、関数が出力されていることを確認した。

```plaintext
0000000067881620 T hs_init_wrapper
0000000067886c20 T hs_exit
0000000067881580 T plus_one
```

# 3. UnityでのDLLの利用

生成したDLLをUnityで使ってみた。
開発時のEditorの挙動で少し厄介なことがあったので、それの対策が必要だった。

## 3.1. DLLの配置

Assetsフォルダ配下にPluginsフォルダを作成し、そこにDLLを配置すればUnityが認識してくれる。
今回は素直に*Assets/Plugins/mylib.dll*に配置した。

## 3.2. 各種設定を64ビットにする

生成されたDLLは64ビットのバイナリなので、Platform settings > Windows > x86_x64 だけにチェックを入れる。

![dll-64bit.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104054/f9e3ff9f-275e-3edf-0ab4-e16403bdbd30.jpeg)

また、Build SettingsのArchitectureをx86_64にしておく。
![architecture.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104054/da22d595-4c2e-deb8-2b90-39432958288d.jpeg)

## 3.3. 利用するコードの記述

以下は適当な変数counterを１秒ごとに１足して、画面上のテキストに反映させるだけのプログラムである。
しかし、１点だけ注意することがある。

```cs:MylibWrapper.cs
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

public class MylibWrapper : MonoBehaviour {

    [DllImport("mylib")]
    public static extern void hs_init_wrapper();
    [DllImport("mylib")]
    public static extern void hs_exit();
    [DllImport("mylib")]
    public static extern int plus_one(int x);

    public Text text; // counterを確認するためのText

    int counter = 0;

    private void Awake() {
        hs_init_wrapper();
    }

    private void Update() {
        counter = plus_one(counter);
        print(counter);
        text.text = $"{counter}";
    }

    void OnApplicationQuit() {
        // エディター上でhs_exit()すると次回のhs_init()でエラー。
        // エディター上ではhs_exit()を実行させないのを一時的な解決方法とする。
#if UNITY_EDITOR

#else
        hs_exit();
#endif
    }
}
```

### エディター上での２度目の起動時に落ちる問題の解決

ghcのランタイムシステムは、hs_exit()した後にhs_init()できないようになっている。
しかし、Unityでは、エディター上でロードされたDLLはUnityを終了するまでアンロードされないため、
エディター上でゲームを開始して、hs_init_wrapper()し、ゲームを停止した時にhs_exit()し、次回のゲーム開始時に再びhs_init_wrapper()を実行するような仕組みにすると、`hs_init_ghc: reinitializing the RTS after shutdown is not currently supported`というエラーが発生して落ちてしまう。

[ソースコード](https://github.com/ghc/ghc/blob/master/rts/RtsStartup.c)を見ると、`rts_shutdown`フラグがこれを拒んでいる。

それであれば、エディター上ではhs_exit()を実行しないという方法しか残されていない。
[ソースコード](https://github.com/ghc/ghc/blob/master/rts/RtsStartup.c)を見ると、hs_init()を複数回実行しても、`hs_init_count`で呼び出し回数をカウントされるものの、２回目以降は何の処理もしておらず、問題ないだろう。（本当に？）
よって、エディター上ではゲーム開始のたびにhs_init_wrapper()が呼ばれるが、ゲーム終了時にはhs_exit()を呼ばないという方針とした。`#if UNITY_EDITOR` ~ `#else` ~ `#endif` の部分。

# 4. 実行

エディター上でも、ビルドした実行ファイルでも、うまく動作した。
![exec.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104054/f3cdcf0a-18c9-1ea4-5549-27d612896bd8.jpeg)

# 5. まとめ

64bitという制約付きであるものの、HaskellのコードをUnity上で動作させることができた。

しかし、いくつか懸念もある。

* 【懸念１】 AndroidやiOSで動作するアプリを作れるのか？

[Mobile Haskell](https://twitter.com/mobilehaskell)というプロジェクトがあるので、これを使ってDLLを作れるかどうかにかかっていそう。検証は苦しそうなので今はしない。

* 【懸念２】 SwitchやPS4で動作するゲームを作れるのか？

それぞれのハードウェアで動作するバイナリ（DLL）が作れるのかどうか。ハードウェアの仕様がわからないので、これも未知数。

Windows上でゲームを公開するだけならば不安は無いが、将来的にモバイル端末やコンシューマー機で公開する可能性がある場合は、これが原因で断念せざるをえなくなる可能性があるので注意が必要。

* 活用方法の検討
  - Haskellでダンジョン生成をしてUnityに取り込む。
  - C#で書きたくない複雑な関数をHaskellで記述してUnityで使う。
  - Haskellでゲームロジックを書いて、全ての描画はUnityに任せる。

* 追記1-Haskell関数からStringを返す（2020/01/05）

戻り値をStringにする方法は[このブログ記事](http://crossframe.iiv.jp/20140309714/)が参考になりました。

* 追記2-エディターが重くなる問題について（2020/01/06）

本記事の*plus_one*のようなシンプルな関数ではなくて、プロシージャルなダンジョンを生成するような、少し複雑な関数を実行したところ、関数の実行後にエディタがかくかくしてしまい、とても開発できるような状況でなくなってしまう問題が発生することがわかりました。原因不明であり、対処方法が判明するまではこの方法はやめておくべきかもしれません。
GHCのランタイムの処理がバックグラウンドで残っていて、Unityの処理を邪魔しているんですかね？どんなコードが問題を発生させるのかを調べたいですが、それが分かったとしても、問題を避けるようなコーディングをせねばならず、そんな不自由なプログラミングはしたくはありませんが。。
ひとまずはオフラインでダンジョンをファイルに書き出しておいて、それをUnityで読み込むという平凡な仕組みにしましたが、筆者はHaskell活用の夢をまだ諦めてはいないぞ。